diff options
| author | Gabriel Falcao <gabriel@nacaolivre.org> | 2013-10-14 01:10:53 -0400 |
|---|---|---|
| committer | Gabriel Falcao <gabriel@nacaolivre.org> | 2013-10-14 01:10:53 -0400 |
| commit | c88bbe9dea6170788a5f4db4bdb9d2fc119fcab0 (patch) | |
| tree | 2d1be892ada39a99c34d2b325f4c9e2e3a0867b2 | |
| download | httpretty-gh-pages.tar.gz | |
documentationgh-pages
64 files changed, 9675 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96d23a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.pyc +.coverage +docs/_build/ +httpretty.egg-info/ +build/ +dist/ +.DS_Store +*.swp +.#* +#* +.tox/ +_public/ +tests/functional/fixtures/recording-*.json diff --git a/.octomarks.yml b/.octomarks.yml new file mode 100644 index 0000000..ffa3cf4 --- /dev/null +++ b/.octomarks.yml @@ -0,0 +1,3 @@ +author_name: Gabriel Falcão +version: 0.6.0 +documentation_index: docs/main.md
\ No newline at end of file diff --git a/.release b/.release new file mode 100755 index 0000000..95a516c --- /dev/null +++ b/.release @@ -0,0 +1,25 @@ +#!/bin/bash + +current_version=$(cat ./docs/.markment.yml | egrep version | sed 's,^[^:]*: *,,g') +printf "The current version is \033[1;33m$current_version\033[0m, type the new version:\n" +read newversion + + +find_files () { + find . -name '*.py' -or -name '*.yml' -or -name '*.md' +} + +update_files (){ + find_files | xargs gsed -i "s,$current_version,$newversion,g" +} + +printf "\033[A\033[A\rI will make a new commit named \033[1;33m'New release $newversion'\033[0m\n" +printf "Are you sure? [\033[1;32myes\033[0m or \033[1;31mno\033[0m]\n" +read sure + +if [ $sure == "yes" ]; then + update_files + printf "New release: \033[1;32m$newversion\033[0m\n" + git add `find_files` + git commit -am "New release: $newversion" +fi; diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7b85357 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python +python: + - "2.6" + - "2.7" +env: + - TEST_TYPE=unit + - TEST_TYPE=functional + +install: + - pip install -r requirements.pip + +script: + - make $TEST_TYPE @@ -0,0 +1,22 @@ +Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..82391d9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include requirements.pip + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f209561 --- /dev/null +++ b/Makefile @@ -0,0 +1,52 @@ +all: check_dependencies unit functional doctests + +filename=httpretty-`python -c 'import httpretty;print httpretty.version'`.tar.gz + +export HTTPRETTY_DEPENDENCIES:= nose sure +export PYTHONPATH:= ${PWD} + +check_dependencies: + @echo "Checking for dependencies to run tests ..." + @for dependency in `echo $$HTTPRETTY_DEPENDENCIES`; do \ + python -c "import $$dependency" 2>/dev/null || (echo "You must install $$dependency in order to run httpretty's tests" && exit 3) ; \ + done + +test: unit functional doctests + +unit: prepare + @echo "Running unit tests ..." + @nosetests -s --verbosity=2 --with-coverage --cover-erase --cover-inclusive tests/unit --cover-package=httpretty + +functional: prepare + @echo "Running functional tests ..." + @nosetests -s --verbosity=2 --with-coverage --cover-erase --cover-inclusive tests/functional --cover-package=httpretty + +doctests: prepare + @echo "Running documentation tests tests ..." + @steadymark README.md + +clean: + @printf "Cleaning up files that are already in .gitignore... " + @for pattern in `cat .gitignore`; do rm -rf $$pattern; done + @echo "OK!" + +release: clean unit functional + @echo "Releasing httpretty..." + @./.release + @python setup.py sdist register upload + +docs: doctests + @markment -o . -t ./theme --sitemap-for="http://falcao.it/HTTPretty" docs + +deploy-docs: + @git co master && \ + (git br -D gh-pages || printf "") && \ + git checkout --orphan gh-pages && \ + markment -o . -t ./theme --sitemap-for="http://falcao.it/HTTPretty" docs && \ + git add . && \ + git commit -am 'documentation' && \ + git push --force origin gh-pages && \ + git checkout master + +prepare: + @reset diff --git a/NEWS.html b/NEWS.html new file mode 100644 index 0000000..dfba68c --- /dev/null +++ b/NEWS.html @@ -0,0 +1,406 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>HTTPretty by gabrielfalcao</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <!-- Loading Bootstrap --> + + <link rel='stylesheet prefetch' href='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.css'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Montserrat:400,700'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700,900,200italic,300italic,400italic,600italic,700italic,900italic'> + <link rel='stylesheet prefetch' href='//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css'> + <link href="./assets/css/github.css" rel="stylesheet"> + <link href="./assets/css/style.css" rel="stylesheet"> + <script src="./assets/js/prefixfree.min.js"></script> + </head> + <body> + + + <div class="uk-container uk-container-center" style="padding: 120px 0;"> + <div class="uk-grid" data-uk-grid-margin=""> + <div class="uk-width-medium-1-4 uk-hidden-small"> + <div style="clear:both; padding-bottom: 50px;"> + <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/httpretty-logo_1.svg" width="240" title="HTTPretty" alt="HTTPretty" /> + <h2>HTTPretty v0.7.0</h2> + <p> + <iframe src="http://instanc.es/bin/btn/watchers-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/forks-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/follow-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> +</p> + <a class="uk-navbar-brand" href="#">Table of Contents</a> + </div> + + <ul style="clear:both;" class="uk-nav"> + + + + + + + + + <li class="uk-nav-header">What is HTTPretty ?</li> + + + + + + <li><a href="intro.html#a-more-technical-description">A more technical description</a></li> + + + + + <li><a href="intro.html#installing">Installing</a></li> + + + + + + + + + + <li><a href="intro.html#expecting-a-simple-response-body">expecting a simple response body</a></li> + + + + + + + + + + <li><a href="intro.html#the-idea-behind-httpretty--how-it-works-">The idea behind HTTPretty (how it works)</a></li> + + + + + + + + + + + + + + + + <li class="uk-nav-header">Reference</li> + + + + + + <li><a href="docs.html#testing-query-strings">testing query strings</a></li> + + + + + <li><a href="docs.html#using-the-decorator">Using the decorator</a></li> + + + + + <li><a href="docs.html#providing-status-code">Providing status code</a></li> + + + + + <li><a href="docs.html#providing-custom-heades">Providing custom heades</a></li> + + + + + + + + + + <li><a href="docs.html#rotating-responses">rotating responses</a></li> + + + + + <li><a href="docs.html#streaming-responses">streaming responses</a></li> + + + + + <li><a href="docs.html#dynamic-responses-through-callbacks">dynamic responses through callbacks</a></li> + + + + + <li><a href="docs.html#matching-regular-expressions">matching regular expressions</a></li> + + + + + <li><a href="docs.html#expect-for-a-response--and-check-the-request-got-by-the--quot-server-quot--to-make-sure-it-was-fine-">expect for a response, and check the request got by the "server" to make sure it was fine.</a></li> + + + + + <li><a href="docs.html#checking-if-is-enabled">checking if is enabled</a></li> + + + + + + + + + + + + + + <li class="uk-nav-header">Acknowledgements</li> + + + + + + <li><a href="about.html#caveats">caveats</a></li> + + + + + + + + + + <li><a href="about.html#supported-libraries">supported libraries</a></li> + + + + + + + + + + + + + + + + + + + + <li class="uk-nav-header">Hacking on HTTPretty</li> + + + + + + <li><a href="contributing.html#creating-a-virtual-env">creating a virtual env</a></li> + + + + + <li><a href="contributing.html#installing-the-dependencies">installing the dependencies</a></li> + + + + + <li><a href="contributing.html#next-steps">next steps</a></li> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <li class="uk-nav-header">Release Notes</li> + + + + + + <li><a href="NEWS.html#0-7-0--current-">0.7.0 (current)</a></li> + + + + + <li><a href="NEWS.html#0-6-5">0.6.5</a></li> + + + + + <li><a href="NEWS.html#0-6-2">0.6.2</a></li> + + + + + <li><a href="NEWS.html#0-6-1">0.6.1</a></li> + + + + + <li><a href="NEWS.html#0-5-14">0.5.14</a></li> + + + + + <li><a href="NEWS.html#0-5-12">0.5.12</a></li> + + + + + <li><a href="NEWS.html#0-5-11">0.5.11</a></li> + + + + + <li><a href="NEWS.html#0-5-10">0.5.10</a></li> + + + + + <li><a href="NEWS.html#0-5-9">0.5.9</a></li> + + + + + <li><a href="NEWS.html#0-5-8">0.5.8</a></li> + + + + + + + + </ul> + </div> + <div class="uk-width-medium-3-4"> + <article class="uk-article"> + <h1 class="uk-article-title">HTTPretty</h1> + <p class="uk-article-lead">HTTP request mock tool for python</p> + <div class="uk-width-medium-1-1"> + <h1 id="release-notes" name="release-notes"><a href="#release-notes">Release Notes</a></h1><h2 id="0-7-0--current-" name="0-7-0--current-"><a href="#0-7-0--current-">0.7.0 (current)</a></h2> +<p>Improvements:</p> + +<ul> +<li>Refactored <code>core.py</code> and increased its unit test coverage to 80%. HTTPretty is slightly more robust now.</li> +</ul> + +<p>Bug fixes:</p> + +<ul> +<li>POST requests being called twice <a href="https://github.com/gabrielfalcao/HTTPretty/pull/100">#100</a></li> +</ul> +<h2 id="0-6-5" name="0-6-5"><a href="#0-6-5">0.6.5</a></h2> +<p>Applied pull requests:</p> + +<ul> +<li>continue on EAGAIN socket errors: <a href="https://github.com/gabrielfalcao/HTTPretty/pull/102">#102</a> by <a href="http://github.com/kouk">kouk</a>.</li> +<li>Fix <code>fake_gethostbyname</code> for requests 2.0: <a href="https://github.com/gabrielfalcao/HTTPretty/pull/101">#101</a> by <a href="http://github.com/mgood">mgood</a></li> +<li>Add a way to match the querystrings: <a href="https://github.com/gabrielfalcao/HTTPretty/pull/98">#98</a> by <a href="http://github.com/ametaireau">ametaireau</a></li> +<li>Use common string case for URIInfo hostname comparison: <a href="https://github.com/gabrielfalcao/HTTPretty/pull/95">#95</a> by <a href="http://github.com/mikewaters">mikewaters</a></li> +<li>Expose httpretty.reset() to public API: <a href="https://github.com/gabrielfalcao/HTTPretty/pull/91">#91</a> by <a href="http://github.com/imankulov">imankulov</a></li> +<li>Don't duplicate http ports number: <a href="https://github.com/gabrielfalcao/HTTPretty/pull/89">#89</a> by <a href="http://github.com/mardiros">mardiros</a></li> +<li>Adding parsed_body parameter to simplify checks: <a href="https://github.com/gabrielfalcao/HTTPretty/pull/88">#88</a> by <a href="http://github.com/toumorokoshi">toumorokoshi</a></li> +<li>Use the real socket if it's not HTTP: <a href="https://github.com/gabrielfalcao/HTTPretty/pull/87">#87</a> by <a href="http://github.com/mardiros">mardiros</a></li> +</ul> +<h2 id="0-6-2" name="0-6-2"><a href="#0-6-2">0.6.2</a></h2> +<ul> +<li>Fixing bug of lack of trailing slashes <a href="https://github.com/gabrielfalcao/HTTPretty/issues/73">#73</a></li> +<li>Applied pull requests <a href="https://github.com/gabrielfalcao/HTTPretty/pull/71">#71</a> and <a href="https://github.com/gabrielfalcao/HTTPretty/pull/72">#72</a> by @andresriancho</li> +<li>Keyword arg coercion fix by @dupuy</li> +<li>@papaeye fixed content-length calculation.</li> +</ul> +<h2 id="0-6-1" name="0-6-1"><a href="#0-6-1">0.6.1</a></h2> +<ul> +<li>New API, no more camel case and everything is available through a simple import:</li> +</ul> +<div class="highlight"><pre name="release-notes-example-1"><span class="kn">import</span> <span class="nn">httpretty</span> + +<span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_function</span><span class="p">():</span> + <span class="c"># httpretty.register_uri(...)</span> + <span class="c"># make request...</span> + <span class="k">pass</span> +</pre></div> +<ul> +<li>Re-organized module into submodules</li> +</ul> +<h2 id="0-5-14" name="0-5-14"><a href="#0-5-14">0.5.14</a></h2> +<ul> +<li><p>Delegate calls to other methods on socket</p></li> +<li><p><a href="https://github.com/gabrielfalcao/HTTPretty/pull/49">Normalized header</a> strings</p></li> +<li><p>Callbacks are <a href="https://github.com/gabrielfalcao/HTTPretty/pull/47">more intelligent now</a></p></li> +<li><p>Normalize urls matching for url quoting</p></li> +</ul> +<h2 id="0-5-12" name="0-5-12"><a href="#0-5-12">0.5.12</a></h2> +<ul> +<li>HTTPretty doesn't hang when using other application protocols under +a @httprettified decorated test.</li> +</ul> +<h2 id="0-5-11" name="0-5-11"><a href="#0-5-11">0.5.11</a></h2> +<ul> +<li>Ability to know whether HTTPretty is or not enabled through <code>httpretty.is_enabled()</code></li> +</ul> +<h2 id="0-5-10" name="0-5-10"><a href="#0-5-10">0.5.10</a></h2> +<ul> +<li>Support to multiple methods per registered URL. Thanks @hughsaunders</li> +</ul> +<h2 id="0-5-9" name="0-5-9"><a href="#0-5-9">0.5.9</a></h2> +<ul> +<li>Fixed python 3 support. Thanks @spulec</li> +</ul> +<h2 id="0-5-8" name="0-5-8"><a href="#0-5-8">0.5.8</a></h2> +<ul> +<li>Support to <a href="#matching-regular-expressions">register regular expressions to match urls</a></li> +<li><a href="#dynamic-responses-through-callbacks">Body callback</a> suppport</li> +<li>Python 3 support</li> +</ul> + + </div> + </div> + </article> + </div> + </div> + </div> + <script type="text/javascript"> + var _gaq = _gaq || []; + _gaq.push(['_setAccount', 'UA-1277640-14']); + _gaq.push(['_trackPageview']); + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); + </script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/jquery-2.0.3.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/angular.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.js'></script> + </body> +</html>
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b938d5e --- /dev/null +++ b/README.md @@ -0,0 +1,463 @@ +# HTTPretty 0.7.0 + + + +[](http://travis-ci.org/gabrielfalcao/HTTPretty) +[](https://bitdeli.com/free "Bitdeli Badge") + +[ChangeLog](NEWS.md) + + +# In a nutshell + +Once upon a time a python developer wanted to use a RESTful api, +everything was fine but until the day he needed to test the code that +hits the RESTful API: what if the API server is down? What if its +content has changed ? + +Don't worry, HTTPretty is here for you: + +```python +import requests +from sure import expect +import httpretty + + +@httpretty.activate +def test_yipit_api_returning_deals(): + httpretty.register_uri(httpretty.GET, "http://api.yipit.com/v1/deals/", + body='[{"title": "Test Deal"}]', + content_type="application/json") + + response = requests.get('http://api.yipit.com/v1/deals/') + + expect(response.json()).to.equal([{"title": "Test Deal"}]) +``` + +# A more technical description + +HTTPretty is a HTTP client mock library for Python 100% inspired on ruby's [FakeWeb](http://fakeweb.rubyforge.org/). +If you come from ruby this would probably sound familiar :) + +# Usage + +## expecting a simple response body + +```python +import requests +import httpretty + +def test_one(): + httpretty.enable() # enable HTTPretty so that it will monkey patch the socket module + httpretty.register_uri(httpretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + response = requests.get('http://yipit.com') + + assert response.text == "Find the best daily deals" + + httpretty.disable() # disable afterwards, so that you will have no problems in code that uses that socket module + httpretty.reset() # reset HTTPretty state (clean up registered urls and request history) +``` + +## testing query strings + +```python +import requests +from sure import expect +import httpretty + +def test_one(): + httpretty.enable() # enable HTTPretty so that it will monkey patch the socket module + httpretty.register_uri(httpretty.GET, "http://yipit.com/login", + body="Find the best daily deals") + + requests.get('http://yipit.com/login?email=user@github.com&password=foobar123') + expect(httpretty.last_request()).to.have.property("querystring").being.equal({ + "email": "user@github.com", + "password": "foobar123", + }) + + httpretty.disable() # disable afterwards, so that you will have no problems in code that uses that socket module +``` + + +## ohhhh, really? can that be easier? + +**YES** we've got a decorator + +```python +import requests +import httpretty + +@httpretty.activate +def test_one(): + httpretty.register_uri(httpretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + response = requests.get('http://yipit.com') + assert response.text == "Find the best daily deals" +``` + +the `@httpretty.activate` is a short-hand decorator that wraps the +decorated function with httpretty.enable() and then calls +httpretty.disable() right after. + +## mocking the status code + +```python +import requests +from sure import expect +import httpretty + +@httpretty.activate +def test_github_access(): + httpretty.register_uri(httpretty.GET, "http://github.com/", + body="here is the mocked body", + status=201) + + response = requests.get('http://github.com') + expect(response.status_code).to.equal(201) +``` + +## you can tell HTTPretty to return any HTTP headers you want + +**and all you need is to add keyword args in which the keys are always lower-cased and with underscores `_` instead of dashes `-`** + +For example, let's say you want to mock that server returns `content-type`. +To do so, use the argument `content_type`, **all the keyword args are taken by HTTPretty and transformed in the RFC2616 equivalent name**. + +```python +@httpretty.activate +def test_some_api(): + httpretty.register_uri(httpretty.GET, "http://foo-api.com/gabrielfalcao", + body='{"success": false}', + status=500, + content_type='text/json') + + response = requests.get('http://foo-api.com/gabrielfalcao') + + expect(response.json()).to.equal({'success': False}) + expect(response.status_code).to.equal(500) +``` + + +### Adding extra headers and forcing headers + +You can pass the `adding_headers` argument as a dictionary and your +headers will be +[united](http://en.wikipedia.org/wiki/Union_(set_theory)) to the +existing headers. + +```python +@httpretty.activate +def test_some_api(): + httpretty.register_uri(httpretty.GET, "http://foo-api.com/gabrielfalcao", + body='{"success": false}', + status=500, + content_type='text/json', + adding_headers={ + 'X-foo': 'bar' + }) + + response = requests.get('http://foo-api.com/gabrielfalcao') + + expect(response.json()).to.equal({'success': False}) + expect(response.status_code).to.equal(500) +``` + +Although there are some situation where some headers line +`content-length` will be calculated by HTTPretty based on the +specified fake response body. + +So you might want to *"force"* those headers: + +```python +@httpretty.activate +def test_some_api(): + httpretty.register_uri(httpretty.GET, "http://foo-api.com/gabrielfalcao", + body='{"success": false}', + status=500, + content_type='text/json', + forcing_headers={ + 'content-length': '100' + }) + + response = requests.get('http://foo-api.com/gabrielfalcao') + + expect(response.json()).to.equal({'success': False}) + expect(response.status_code).to.equal(500) +``` + +You should, though, be careful with it. The HTTP client is likely to +rely on the content length to know how many bytes of response payload +should be loaded. Forcing a `content-length` that is bigger than the +action response body might cause the HTTP client to hang because it is +waiting for data. Read more in the "caveats" session on the bottom. + +## rotating responses + +Same URL, same request method, the first request return the first +httpretty.Response, all the subsequent ones return the last (status 202). + +Notice that the `responses` argument is a list and you can pass as +many responses as you want. + +```python +import requests +from sure import expect + + +@httpretty.activate +def test_rotating_responses(): + httpretty.register_uri(httpretty.GET, "http://github.com/gabrielfalcao/httpretty", + responses=[ + httpretty.Response(body="first response", status=201), + httpretty.Response(body='second and last response', status=202), + ]) + + response1 = requests.get('http://github.com/gabrielfalcao/httpretty') + expect(response1.status_code).to.equal(201) + expect(response1.text).to.equal('first response') + + response2 = requests.get('http://github.com/gabrielfalcao/httpretty') + expect(response2.status_code).to.equal(202) + expect(response2.text).to.equal('second and last response') + + response3 = requests.get('http://github.com/gabrielfalcao/httpretty') + + expect(response3.status_code).to.equal(202) + expect(response3.text).to.equal('second and last response') +``` +## streaming responses + +Mock a streaming response by registering a generator response body. + +```python +import requests +from sure import expect +import httpretty + +# mock a streaming response body with a generator +def mock_streaming_tweets(tweets): + from time import sleep + for t in tweets: + sleep(.5) + yield t + +@httpretty.activate +def test_twitter_api_integration(now): + twitter_response_lines = [ + '{"text":"If @BarackObama requests to follow me one more time I\'m calling the police."}\r\n', + '\r\n', + '{"text":"Thanks for all your #FollowMe1D requests Directioners! We\u2019ll be following 10 people throughout the day starting NOW. G ..."}\r\n' + ] + + TWITTER_STREAMING_URL = "https://stream.twitter.com/1/statuses/filter.json" + + # set the body to a generator and set `streaming=True` to mock a streaming response body + httpretty.register_uri(httpretty.POST, TWITTER_STREAMING_URL, + body=mock_streaming_tweets(twitter_response_lines), + streaming=True) + + # taken from the requests docs + # http://docs.python-requests.org/en/latest/user/advanced/#streaming-requests + response = requests.post(TWITTER_STREAMING_URL, data={'track':'requests'}, + auth=('username','password'), prefetch=False) + + #test iterating by line + line_iter = response.iter_lines() + for i in xrange(len(twitter_response_lines)): + expect(line_iter.next().strip()).to.equal(twitter_response_lines[i].strip()) +``` + +## dynamic responses through callbacks + +Set a callback to allow for dynamic responses based on the request. + +```python +import requests +from sure import expect +import httpretty + +@httpretty.activate +def test_response_callbacks(): + + def request_callback(method, uri, headers): + return (200, headers, "The {} response from {}".format(method, uri)) + + httpretty.register_uri( + httpretty.GET, "https://api.yahoo.com/test", + body=request_callback) + + response = requests.get('https://api.yahoo.com/test') + + expect(response.text).to.equal('The GET response from https://api.yahoo.com/test') +``` + +## matching regular expressions + +You can register a +[compiled regex](http://docs.python.org/2/library/re.html#re.compile) +and it will be matched against the requested urls. + +```python +@httpretty.activate +def test_httpretty_should_allow_registering_regexes(): + u"HTTPretty should allow registering regexes" + + httpretty.register_uri( + httpretty.GET, + re.compile("api.yipit.com/v2/deal;brand=(\w+)"), + body="Found brand", + ) + + response = requests.get('https://api.yipit.com/v2/deal;brand=GAP') + expect(response.text).to.equal('Found brand') + expect(httpretty.last_request().method).to.equal('GET') + expect(httpretty.last_request().path).to.equal('/v1/deal;brand=GAP') +``` + +By default, the regexp you register will match the requests without looking at +the querystring. If you want the querystring to be considered, you can set +`match_querystring=True` when calling `register_uri`. + +## expect for a response, and check the request got by the "server" to make sure it was fine. + +```python +import requests +from sure import expect +import httpretty + + +@httpretty.activate +def test_yipit_api_integration(): + httpretty.register_uri(httpretty.POST, "http://api.yipit.com/foo/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + response = requests.post('http://api.yipit.com/foo', + '{"username": "gabrielfalcao"}', + headers={ + 'content-type': 'text/json', + }) + + expect(response.text).to.equal('{"repositories": ["HTTPretty", "lettuce"]}') + expect(httpretty.last_request().method).to.equal("POST") + expect(httpretty.last_request().headers['content-type']).to.equal('text/json') +``` + +## checking if is enabled + +```python + +httpretty.enable() +httpretty.is_enabled().should.be.true + +httpretty.disable() +httpretty.is_enabled().should.be.false + +``` +# Motivation + +When building systems that access external resources such as RESTful +webservices, XMLRPC or even simple HTTP requests, we stumble in the +problem: + + "I'm gonna need to mock all those requests" + +It brings a lot of hassle, you will need to use a generic mocking +tool, mess with scope and so on. + +## The idea behind HTTPretty (how it works) + +HTTPretty [monkey patches](http://en.wikipedia.org/wiki/Monkey_patch) +Python's [socket](http://docs.python.org/library/socket.html) core +module, reimplementing the HTTP protocol, by mocking requests and +responses. + +As for it works in this way, you don't need to worry what http library +you're gonna use. + +HTTPretty will mock the response for you :) *(and also give you the latest requests so that you can check them)* + +# Acknowledgements + +## caveats with the [requests](http://docs.python-requests.org/en/latest/) library + +### `forcing_headers` + `Content-Length` + +if you use the `forcing_headers` options make sure to add the header +`Content-Length` otherwise the +[requests](http://docs.python-requests.org/en/latest/) will try to +load the response endlessly + +# Officially supported libraries + +Because HTTPretty works in the socket level it should work with any HTTP client libraries, although it is [battle tested](https://github.com/gabrielfalcao/HTTPretty/tree/master/tests/functional) against: + +* [requests](http://docs.python-requests.org/en/latest/) +* [httplib2](http://code.google.com/p/httplib2/) +* [urllib2](http://docs.python.org/2/library/urllib2.html) + +# Hacking on HTTPretty + +#### create a virtual env + +you will need [virtualenvwrapper](http://www.doughellmann.com/projects/virtualenvwrapper/) + + +```console +mkvirtualenv --distribute --no-site-packages HTTPretty +``` + +#### install the dependencies + +```console +pip install -r requirements.pip +``` + +#### next steps: + +1. run the tests with make: +```bash +make unit functional +``` +2. hack at will +3. commit, push etc +4. send a pull request + +# License + + <HTTPretty - HTTP client mock for Python> + Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + +# Main contributors + +There folks made remarkable contributions to HTTPretty: + +* Steve Pulec ~> @spulec +* Hugh Saunders ~> @hughsaunders +* Matt Luongo ~> @mhluongo +* James Rowe ~> @JNRowe diff --git a/about.html b/about.html new file mode 100644 index 0000000..89dff18 --- /dev/null +++ b/about.html @@ -0,0 +1,342 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>HTTPretty by gabrielfalcao</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <!-- Loading Bootstrap --> + + <link rel='stylesheet prefetch' href='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.css'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Montserrat:400,700'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700,900,200italic,300italic,400italic,600italic,700italic,900italic'> + <link rel='stylesheet prefetch' href='//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css'> + <link href="./assets/css/github.css" rel="stylesheet"> + <link href="./assets/css/style.css" rel="stylesheet"> + <script src="./assets/js/prefixfree.min.js"></script> + </head> + <body> + + + <div class="uk-container uk-container-center" style="padding: 120px 0;"> + <div class="uk-grid" data-uk-grid-margin=""> + <div class="uk-width-medium-1-4 uk-hidden-small"> + <div style="clear:both; padding-bottom: 50px;"> + <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/httpretty-logo_1.svg" width="240" title="HTTPretty" alt="HTTPretty" /> + <h2>HTTPretty v0.7.0</h2> + <p> + <iframe src="http://instanc.es/bin/btn/watchers-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/forks-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/follow-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> +</p> + <a class="uk-navbar-brand" href="#">Table of Contents</a> + </div> + + <ul style="clear:both;" class="uk-nav"> + + + + + + + + + <li class="uk-nav-header">What is HTTPretty ?</li> + + + + + + <li><a href="intro.html#a-more-technical-description">A more technical description</a></li> + + + + + <li><a href="intro.html#installing">Installing</a></li> + + + + + + + + + + <li><a href="intro.html#expecting-a-simple-response-body">expecting a simple response body</a></li> + + + + + + + + + + <li><a href="intro.html#the-idea-behind-httpretty--how-it-works-">The idea behind HTTPretty (how it works)</a></li> + + + + + + + + + + + + + + + + <li class="uk-nav-header">Reference</li> + + + + + + <li><a href="docs.html#testing-query-strings">testing query strings</a></li> + + + + + <li><a href="docs.html#using-the-decorator">Using the decorator</a></li> + + + + + <li><a href="docs.html#providing-status-code">Providing status code</a></li> + + + + + <li><a href="docs.html#providing-custom-heades">Providing custom heades</a></li> + + + + + + + + + + <li><a href="docs.html#rotating-responses">rotating responses</a></li> + + + + + <li><a href="docs.html#streaming-responses">streaming responses</a></li> + + + + + <li><a href="docs.html#dynamic-responses-through-callbacks">dynamic responses through callbacks</a></li> + + + + + <li><a href="docs.html#matching-regular-expressions">matching regular expressions</a></li> + + + + + <li><a href="docs.html#expect-for-a-response--and-check-the-request-got-by-the--quot-server-quot--to-make-sure-it-was-fine-">expect for a response, and check the request got by the "server" to make sure it was fine.</a></li> + + + + + <li><a href="docs.html#checking-if-is-enabled">checking if is enabled</a></li> + + + + + + + + + + + + + + <li class="uk-nav-header">Acknowledgements</li> + + + + + + <li><a href="about.html#caveats">caveats</a></li> + + + + + + + + + + <li><a href="about.html#supported-libraries">supported libraries</a></li> + + + + + + + + + + + + + + + + + + + + <li class="uk-nav-header">Hacking on HTTPretty</li> + + + + + + <li><a href="contributing.html#creating-a-virtual-env">creating a virtual env</a></li> + + + + + <li><a href="contributing.html#installing-the-dependencies">installing the dependencies</a></li> + + + + + <li><a href="contributing.html#next-steps">next steps</a></li> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <li class="uk-nav-header">Release Notes</li> + + + + + + <li><a href="NEWS.html#0-7-0--current-">0.7.0 (current)</a></li> + + + + + <li><a href="NEWS.html#0-6-5">0.6.5</a></li> + + + + + <li><a href="NEWS.html#0-6-2">0.6.2</a></li> + + + + + <li><a href="NEWS.html#0-6-1">0.6.1</a></li> + + + + + <li><a href="NEWS.html#0-5-14">0.5.14</a></li> + + + + + <li><a href="NEWS.html#0-5-12">0.5.12</a></li> + + + + + <li><a href="NEWS.html#0-5-11">0.5.11</a></li> + + + + + <li><a href="NEWS.html#0-5-10">0.5.10</a></li> + + + + + <li><a href="NEWS.html#0-5-9">0.5.9</a></li> + + + + + <li><a href="NEWS.html#0-5-8">0.5.8</a></li> + + + + + + + + </ul> + </div> + <div class="uk-width-medium-3-4"> + <article class="uk-article"> + <h1 class="uk-article-title">HTTPretty</h1> + <p class="uk-article-lead">HTTP request mock tool for python</p> + <div class="uk-width-medium-1-1"> + <h1 id="acknowledgements" name="acknowledgements"><a href="#acknowledgements">Acknowledgements</a></h1><h2 id="caveats" name="caveats"><a href="#caveats">caveats</a></h2><h3 id="-code-forcing_headers--code-----code-content-length--code-" name="-code-forcing_headers--code-----code-content-length--code-"><a href="#-code-forcing_headers--code-----code-content-length--code-"><code>forcing_headers</code> + <code>Content-Length</code></a></h3> +<p>if you use the <code>forcing_headers</code> options make sure to add the header +<code>Content-Length</code> otherwise the +<a href="http://docs.python-requests.org/en/latest/">requests</a> will try to +load the response endlessly</p> +<h2 id="supported-libraries" name="supported-libraries"><a href="#supported-libraries">supported libraries</a></h2> +<p>Because HTTPretty works in the socket level it should work with any HTTP client libraries, although it is <a href="https://github.com/gabrielfalcao/HTTPretty/tree/master/tests/functional">battle tested</a> against:</p> + +<ul> +<li><a href="http://docs.python-requests.org/en/latest/">requests</a></li> +<li><a href="http://code.google.com/p/httplib2/">httplib2</a></li> +<li><a href="http://docs.python.org/2/library/urllib2.html">urllib2</a></li> +</ul> + + </div> + </div> + </article> + </div> + </div> + </div> + <script type="text/javascript"> + var _gaq = _gaq || []; + _gaq.push(['_setAccount', 'UA-1277640-14']); + _gaq.push(['_trackPageview']); + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); + </script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/jquery-2.0.3.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/angular.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.js'></script> + </body> +</html>
\ No newline at end of file diff --git a/assets/README.txt b/assets/README.txt new file mode 100644 index 0000000..e5b5b41 --- /dev/null +++ b/assets/README.txt @@ -0,0 +1,3 @@ +A Pen created at CodePen.io. You can find this one at http://codepen.io/gabrielfalcao/pen/Esdub. + +
\ No newline at end of file diff --git a/assets/css/github.css b/assets/css/github.css new file mode 100644 index 0000000..dc60655 --- /dev/null +++ b/assets/css/github.css @@ -0,0 +1,61 @@ +.hll { background-color: #ffffcc } +.c { color: #999988; font-style: italic } /* Comment */ +.err { color: #a61717; background-color: #e3d2d2 } /* Error */ +.k { color: #000000; font-weight: bold } /* Keyword */ +.o { color: #000000; font-weight: bold } /* Operator */ +.cm { color: #999988; font-style: italic } /* Comment.Multiline */ +.cp { color: #999999; font-weight: bold; font-style: italic } /* Comment.Preproc */ +.c1 { color: #999988; font-style: italic } /* Comment.Single */ +.cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ +.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ +.ge { color: #000000; font-style: italic } /* Generic.Emph */ +.gr { color: #aa0000 } /* Generic.Error */ +.gh { color: #999999 } /* Generic.Heading */ +.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ +.go { color: #888888 } /* Generic.Output */ +.gp { color: #555555 } /* Generic.Prompt */ +.gs { font-weight: bold } /* Generic.Strong */ +.gu { color: #aaaaaa } /* Generic.Subheading */ +.gt { color: #aa0000 } /* Generic.Traceback */ +.kc { color: #000000; font-weight: bold } /* Keyword.Constant */ +.kd { color: #000000; font-weight: bold } /* Keyword.Declaration */ +.kn { color: #000000; font-weight: bold } /* Keyword.Namespace */ +.kp { color: #000000; font-weight: bold } /* Keyword.Pseudo */ +.kr { color: #000000; font-weight: bold } /* Keyword.Reserved */ +.kt { color: #445588; font-weight: bold } /* Keyword.Type */ +.m { color: #009999 } /* Literal.Number */ +.s { color: #d01040 } /* Literal.String */ +.na { color: #008080 } /* Name.Attribute */ +.nb { color: #0086B3 } /* Name.Builtin */ +.nc { color: #445588; font-weight: bold } /* Name.Class */ +.no { color: #008080 } /* Name.Constant */ +.nd { color: #3c5d5d; font-weight: bold } /* Name.Decorator */ +.ni { color: #800080 } /* Name.Entity */ +.ne { color: #990000; font-weight: bold } /* Name.Exception */ +.nf { color: #990000; font-weight: bold } /* Name.Function */ +.nl { color: #990000; font-weight: bold } /* Name.Label */ +.nn { color: #555555 } /* Name.Namespace */ +.nt { color: #000080 } /* Name.Tag */ +.nv { color: #008080 } /* Name.Variable */ +.ow { color: #000000; font-weight: bold } /* Operator.Word */ +.w { color: #bbbbbb } /* Text.Whitespace */ +.mf { color: #009999 } /* Literal.Number.Float */ +.mh { color: #009999 } /* Literal.Number.Hex */ +.mi { color: #009999 } /* Literal.Number.Integer */ +.mo { color: #009999 } /* Literal.Number.Oct */ +.sb { color: #d01040 } /* Literal.String.Backtick */ +.sc { color: #d01040 } /* Literal.String.Char */ +.sd { color: #d01040 } /* Literal.String.Doc */ +.s2 { color: #d01040 } /* Literal.String.Double */ +.se { color: #d01040 } /* Literal.String.Escape */ +.sh { color: #d01040 } /* Literal.String.Heredoc */ +.si { color: #d01040 } /* Literal.String.Interpol */ +.sx { color: #d01040 } /* Literal.String.Other */ +.sr { color: #009926 } /* Literal.String.Regex */ +.s1 { color: #d01040 } /* Literal.String.Single */ +.ss { color: #990073 } /* Literal.String.Symbol */ +.bp { color: #999999 } /* Name.Builtin.Pseudo */ +.vc { color: #008080 } /* Name.Variable.Class */ +.vg { color: #008080 } /* Name.Variable.Global */ +.vi { color: #008080 } /* Name.Variable.Instance */ +.il { color: #009999 } /* Literal.Number.Integer.Long */ diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..17a3813 --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,68 @@ +html { + background: #faf9f4; +} +body { + background: transparent; + top: 0; + bottom: 0; + left: 0; + right: 0; +} +.logo { + padding-top: 30px; +} +.logo h1 { + font-size: 60px; +} +.logo h1 small { + font-size: 30px; +} +*, +body, +html, +.article, +h1, +h2, +h3, +h4, +h5, +h6, +p, +div, +span, +strong, +a, +li, +ul, +ol, +nav { + font-family: "Montserrat"; +} +.httpretty-navbar .uk-navbar-brand { + font-size: 28px; + padding-top: 4px; +} +.httpretty-navbar .uk-navbar-brand small { + font-size: 14px; +} +a.uk-button-primary { + color: white; +} + + +a { + color: #3973A6; +} + +code, code *, pre, pre * { + font-family: "Monaco", "Inconsolata", "monospace"; +} + +div.highlight pre { + box-shadow: inset 0px 1px 2px 1px rgba(80, 80, 80, .15); +} + + +#docs-body { + padding-top: 120px; +} diff --git a/assets/js/prefixfree.min.js b/assets/js/prefixfree.min.js new file mode 100644 index 0000000..9512edd --- /dev/null +++ b/assets/js/prefixfree.min.js @@ -0,0 +1,492 @@ +/** + * StyleFix 1.0.3 & PrefixFree 1.0.7 + * @author Lea Verou + * MIT license + */ + +(function(){ + +if(!window.addEventListener) { + return; +} + +var self = window.StyleFix = { + link: function(link) { + try { + // Ignore stylesheets with data-noprefix attribute as well as alternate stylesheets + if(link.rel !== 'stylesheet' || link.hasAttribute('data-noprefix')) { + return; + } + } + catch(e) { + return; + } + + var url = link.href || link.getAttribute('data-href'), + base = url.replace(/[^\/]+$/, ''), + base_scheme = (/^[a-z]{3,10}:/.exec(base) || [''])[0], + base_domain = (/^[a-z]{3,10}:\/\/[^\/]+/.exec(base) || [''])[0], + base_query = /^([^?]*)\??/.exec(url)[1], + parent = link.parentNode, + xhr = new XMLHttpRequest(), + process; + + xhr.onreadystatechange = function() { + if(xhr.readyState === 4) { + process(); + } + }; + + process = function() { + var css = xhr.responseText; + + if(css && link.parentNode && (!xhr.status || xhr.status < 400 || xhr.status > 600)) { + css = self.fix(css, true, link); + + // Convert relative URLs to absolute, if needed + if(base) { + css = css.replace(/url\(\s*?((?:"|')?)(.+?)\1\s*?\)/gi, function($0, quote, url) { + if(/^([a-z]{3,10}:|#)/i.test(url)) { // Absolute & or hash-relative + return $0; + } + else if(/^\/\//.test(url)) { // Scheme-relative + // May contain sequences like /../ and /./ but those DO work + return 'url("' + base_scheme + url + '")'; + } + else if(/^\//.test(url)) { // Domain-relative + return 'url("' + base_domain + url + '")'; + } + else if(/^\?/.test(url)) { // Query-relative + return 'url("' + base_query + url + '")'; + } + else { + // Path-relative + return 'url("' + base + url + '")'; + } + }); + + // behavior URLs shoudn’t be converted (Issue #19) + // base should be escaped before added to RegExp (Issue #81) + var escaped_base = base.replace(/([\\\^\$*+[\]?{}.=!:(|)])/g,"\\$1"); + css = css.replace(RegExp('\\b(behavior:\\s*?url\\(\'?"?)' + escaped_base, 'gi'), '$1'); + } + + var style = document.createElement('style'); + style.textContent = css; + style.media = link.media; + style.disabled = link.disabled; + style.setAttribute('data-href', link.getAttribute('href')); + + parent.insertBefore(style, link); + parent.removeChild(link); + + style.media = link.media; // Duplicate is intentional. See issue #31 + } + }; + + try { + xhr.open('GET', url); + xhr.send(null); + } catch (e) { + // Fallback to XDomainRequest if available + if (typeof XDomainRequest != "undefined") { + xhr = new XDomainRequest(); + xhr.onerror = xhr.onprogress = function() {}; + xhr.onload = process; + xhr.open("GET", url); + xhr.send(null); + } + } + + link.setAttribute('data-inprogress', ''); + }, + + styleElement: function(style) { + if (style.hasAttribute('data-noprefix')) { + return; + } + var disabled = style.disabled; + + style.textContent = self.fix(style.textContent, true, style); + + style.disabled = disabled; + }, + + styleAttribute: function(element) { + var css = element.getAttribute('style'); + + css = self.fix(css, false, element); + + element.setAttribute('style', css); + }, + + process: function() { + // Linked stylesheets + $('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link); + + // Inline stylesheets + $('style').forEach(StyleFix.styleElement); + + // Inline styles + $('[style]').forEach(StyleFix.styleAttribute); + }, + + register: function(fixer, index) { + (self.fixers = self.fixers || []) + .splice(index === undefined? self.fixers.length : index, 0, fixer); + }, + + fix: function(css, raw, element) { + for(var i=0; i<self.fixers.length; i++) { + css = self.fixers[i](css, raw, element) || css; + } + + return css; + }, + + camelCase: function(str) { + return str.replace(/-([a-z])/g, function($0, $1) { return $1.toUpperCase(); }).replace('-',''); + }, + + deCamelCase: function(str) { + return str.replace(/[A-Z]/g, function($0) { return '-' + $0.toLowerCase() }); + } +}; + +/************************************** + * Process styles + **************************************/ +(function(){ + setTimeout(function(){ + $('link[rel="stylesheet"]').forEach(StyleFix.link); + }, 10); + + document.addEventListener('DOMContentLoaded', StyleFix.process, false); +})(); + +function $(expr, con) { + return [].slice.call((con || document).querySelectorAll(expr)); +} + +})(); + +/** + * PrefixFree + */ +(function(root){ + +if(!window.StyleFix || !window.getComputedStyle) { + return; +} + +// Private helper +function fix(what, before, after, replacement, css) { + what = self[what]; + + if(what.length) { + var regex = RegExp(before + '(' + what.join('|') + ')' + after, 'gi'); + + css = css.replace(regex, replacement); + } + + return css; +} + +var self = window.PrefixFree = { + prefixCSS: function(css, raw, element) { + var prefix = self.prefix; + + // Gradient angles hotfix + if(self.functions.indexOf('linear-gradient') > -1) { + // Gradients are supported with a prefix, convert angles to legacy + css = css.replace(/(\s|:|,)(repeating-)?linear-gradient\(\s*(-?\d*\.?\d*)deg/ig, function ($0, delim, repeating, deg) { + return delim + (repeating || '') + 'linear-gradient(' + (90-deg) + 'deg'; + }); + } + + css = fix('functions', '(\\s|:|,)', '\\s*\\(', '$1' + prefix + '$2(', css); + css = fix('keywords', '(\\s|:)', '(\\s|;|\\}|$)', '$1' + prefix + '$2$3', css); + css = fix('properties', '(^|\\{|\\s|;)', '\\s*:', '$1' + prefix + '$2:', css); + + // Prefix properties *inside* values (issue #8) + if (self.properties.length) { + var regex = RegExp('\\b(' + self.properties.join('|') + ')(?!:)', 'gi'); + + css = fix('valueProperties', '\\b', ':(.+?);', function($0) { + return $0.replace(regex, prefix + "$1") + }, css); + } + + if(raw) { + css = fix('selectors', '', '\\b', self.prefixSelector, css); + css = fix('atrules', '@', '\\b', '@' + prefix + '$1', css); + } + + // Fix double prefixing + css = css.replace(RegExp('-' + prefix, 'g'), '-'); + + // Prefix wildcard + css = css.replace(/-\*-(?=[a-z]+)/gi, self.prefix); + + return css; + }, + + property: function(property) { + return (self.properties.indexOf(property)? self.prefix : '') + property; + }, + + value: function(value, property) { + value = fix('functions', '(^|\\s|,)', '\\s*\\(', '$1' + self.prefix + '$2(', value); + value = fix('keywords', '(^|\\s)', '(\\s|$)', '$1' + self.prefix + '$2$3', value); + + // TODO properties inside values + + return value; + }, + + // Warning: Prefixes no matter what, even if the selector is supported prefix-less + prefixSelector: function(selector) { + return selector.replace(/^:{1,2}/, function($0) { return $0 + self.prefix }) + }, + + // Warning: Prefixes no matter what, even if the property is supported prefix-less + prefixProperty: function(property, camelCase) { + var prefixed = self.prefix + property; + + return camelCase? StyleFix.camelCase(prefixed) : prefixed; + } +}; + +/************************************** + * Properties + **************************************/ +(function() { + var prefixes = {}, + properties = [], + shorthands = {}, + style = getComputedStyle(document.documentElement, null), + dummy = document.createElement('div').style; + + // Why are we doing this instead of iterating over properties in a .style object? Cause Webkit won't iterate over those. + var iterate = function(property) { + if(property.charAt(0) === '-') { + properties.push(property); + + var parts = property.split('-'), + prefix = parts[1]; + + // Count prefix uses + prefixes[prefix] = ++prefixes[prefix] || 1; + + // This helps determining shorthands + while(parts.length > 3) { + parts.pop(); + + var shorthand = parts.join('-'); + + if(supported(shorthand) && properties.indexOf(shorthand) === -1) { + properties.push(shorthand); + } + } + } + }, + supported = function(property) { + return StyleFix.camelCase(property) in dummy; + } + + // Some browsers have numerical indices for the properties, some don't + if(style.length > 0) { + for(var i=0; i<style.length; i++) { + iterate(style[i]) + } + } + else { + for(var property in style) { + iterate(StyleFix.deCamelCase(property)); + } + } + + // Find most frequently used prefix + var highest = {uses:0}; + for(var prefix in prefixes) { + var uses = prefixes[prefix]; + + if(highest.uses < uses) { + highest = {prefix: prefix, uses: uses}; + } + } + + self.prefix = '-' + highest.prefix + '-'; + self.Prefix = StyleFix.camelCase(self.prefix); + + self.properties = []; + + // Get properties ONLY supported with a prefix + for(var i=0; i<properties.length; i++) { + var property = properties[i]; + + if(property.indexOf(self.prefix) === 0) { // we might have multiple prefixes, like Opera + var unprefixed = property.slice(self.prefix.length); + + if(!supported(unprefixed)) { + self.properties.push(unprefixed); + } + } + } + + // IE fix + if(self.Prefix == 'Ms' + && !('transform' in dummy) + && !('MsTransform' in dummy) + && ('msTransform' in dummy)) { + self.properties.push('transform', 'transform-origin'); + } + + self.properties.sort(); +})(); + +/************************************** + * Values + **************************************/ +(function() { +// Values that might need prefixing +var functions = { + 'linear-gradient': { + property: 'backgroundImage', + params: 'red, teal' + }, + 'calc': { + property: 'width', + params: '1px + 5%' + }, + 'element': { + property: 'backgroundImage', + params: '#foo' + }, + 'cross-fade': { + property: 'backgroundImage', + params: 'url(a.png), url(b.png), 50%' + } +}; + + +functions['repeating-linear-gradient'] = +functions['repeating-radial-gradient'] = +functions['radial-gradient'] = +functions['linear-gradient']; + +// Note: The properties assigned are just to *test* support. +// The keywords will be prefixed everywhere. +var keywords = { + 'initial': 'color', + 'zoom-in': 'cursor', + 'zoom-out': 'cursor', + 'box': 'display', + 'flexbox': 'display', + 'inline-flexbox': 'display', + 'flex': 'display', + 'inline-flex': 'display', + 'grid': 'display', + 'inline-grid': 'display', + 'min-content': 'width' +}; + +self.functions = []; +self.keywords = []; + +var style = document.createElement('div').style; + +function supported(value, property) { + style[property] = ''; + style[property] = value; + + return !!style[property]; +} + +for (var func in functions) { + var test = functions[func], + property = test.property, + value = func + '(' + test.params + ')'; + + if (!supported(value, property) + && supported(self.prefix + value, property)) { + // It's supported, but with a prefix + self.functions.push(func); + } +} + +for (var keyword in keywords) { + var property = keywords[keyword]; + + if (!supported(keyword, property) + && supported(self.prefix + keyword, property)) { + // It's supported, but with a prefix + self.keywords.push(keyword); + } +} + +})(); + +/************************************** + * Selectors and @-rules + **************************************/ +(function() { + +var +selectors = { + ':read-only': null, + ':read-write': null, + ':any-link': null, + '::selection': null +}, + +atrules = { + 'keyframes': 'name', + 'viewport': null, + 'document': 'regexp(".")' +}; + +self.selectors = []; +self.atrules = []; + +var style = root.appendChild(document.createElement('style')); + +function supported(selector) { + style.textContent = selector + '{}'; // Safari 4 has issues with style.innerHTML + + return !!style.sheet.cssRules.length; +} + +for(var selector in selectors) { + var test = selector + (selectors[selector]? '(' + selectors[selector] + ')' : ''); + + if(!supported(test) && supported(self.prefixSelector(test))) { + self.selectors.push(selector); + } +} + +for(var atrule in atrules) { + var test = atrule + ' ' + (atrules[atrule] || ''); + + if(!supported('@' + test) && supported('@' + self.prefix + test)) { + self.atrules.push(atrule); + } +} + +root.removeChild(style); + +})(); + +// Properties that accept properties as their value +self.valueProperties = [ + 'transition', + 'transition-property' +] + +// Add class for current prefix +root.className += ' ' + self.prefix; + +StyleFix.register(self.prefixCSS); + + +})(document.documentElement);
\ No newline at end of file diff --git a/assets/less/style.less b/assets/less/style.less new file mode 100644 index 0000000..b2f68aa --- /dev/null +++ b/assets/less/style.less @@ -0,0 +1,38 @@ +html { + background: #faf9f4; +} +body { + background: transparent; + top: 0; + bottom:0; + left:0; + right:0; +} +.logo { + padding-top: 30px; + h1 { + font-size: 60px; + small { + font-size: 30px; + + } + } +} + +*, body, html, .article, h1, h2, h3, h4, h5, h6, p, div, span, strong, a, li, ul, ol, nav { + font-family: "Montserrat"; +} + +.httpretty-navbar { + .uk-navbar-brand{ + font-size: 28px; + padding-top: 4px; + small { + font-size:14px; + } + } +} + +a.uk-button-primary { + color:white; +}
\ No newline at end of file diff --git a/assets/license.txt b/assets/license.txt new file mode 100644 index 0000000..4a325c7 --- /dev/null +++ b/assets/license.txt @@ -0,0 +1,9 @@ +<!-- +Copyright (c) 2013 by Gabriel Falcão (http://codepen.io/gabrielfalcao/pen/Esdub) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-->
\ No newline at end of file diff --git a/contributing.html b/contributing.html new file mode 100644 index 0000000..4b85f30 --- /dev/null +++ b/contributing.html @@ -0,0 +1,375 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>HTTPretty by gabrielfalcao</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <!-- Loading Bootstrap --> + + <link rel='stylesheet prefetch' href='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.css'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Montserrat:400,700'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700,900,200italic,300italic,400italic,600italic,700italic,900italic'> + <link rel='stylesheet prefetch' href='//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css'> + <link href="./assets/css/github.css" rel="stylesheet"> + <link href="./assets/css/style.css" rel="stylesheet"> + <script src="./assets/js/prefixfree.min.js"></script> + </head> + <body> + + + <div class="uk-container uk-container-center" style="padding: 120px 0;"> + <div class="uk-grid" data-uk-grid-margin=""> + <div class="uk-width-medium-1-4 uk-hidden-small"> + <div style="clear:both; padding-bottom: 50px;"> + <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/httpretty-logo_1.svg" width="240" title="HTTPretty" alt="HTTPretty" /> + <h2>HTTPretty v0.7.0</h2> + <p> + <iframe src="http://instanc.es/bin/btn/watchers-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/forks-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/follow-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> +</p> + <a class="uk-navbar-brand" href="#">Table of Contents</a> + </div> + + <ul style="clear:both;" class="uk-nav"> + + + + + + + + + <li class="uk-nav-header">What is HTTPretty ?</li> + + + + + + <li><a href="intro.html#a-more-technical-description">A more technical description</a></li> + + + + + <li><a href="intro.html#installing">Installing</a></li> + + + + + + + + + + <li><a href="intro.html#expecting-a-simple-response-body">expecting a simple response body</a></li> + + + + + + + + + + <li><a href="intro.html#the-idea-behind-httpretty--how-it-works-">The idea behind HTTPretty (how it works)</a></li> + + + + + + + + + + + + + + + + <li class="uk-nav-header">Reference</li> + + + + + + <li><a href="docs.html#testing-query-strings">testing query strings</a></li> + + + + + <li><a href="docs.html#using-the-decorator">Using the decorator</a></li> + + + + + <li><a href="docs.html#providing-status-code">Providing status code</a></li> + + + + + <li><a href="docs.html#providing-custom-heades">Providing custom heades</a></li> + + + + + + + + + + <li><a href="docs.html#rotating-responses">rotating responses</a></li> + + + + + <li><a href="docs.html#streaming-responses">streaming responses</a></li> + + + + + <li><a href="docs.html#dynamic-responses-through-callbacks">dynamic responses through callbacks</a></li> + + + + + <li><a href="docs.html#matching-regular-expressions">matching regular expressions</a></li> + + + + + <li><a href="docs.html#expect-for-a-response--and-check-the-request-got-by-the--quot-server-quot--to-make-sure-it-was-fine-">expect for a response, and check the request got by the "server" to make sure it was fine.</a></li> + + + + + <li><a href="docs.html#checking-if-is-enabled">checking if is enabled</a></li> + + + + + + + + + + + + + + <li class="uk-nav-header">Acknowledgements</li> + + + + + + <li><a href="about.html#caveats">caveats</a></li> + + + + + + + + + + <li><a href="about.html#supported-libraries">supported libraries</a></li> + + + + + + + + + + + + + + + + + + + + <li class="uk-nav-header">Hacking on HTTPretty</li> + + + + + + <li><a href="contributing.html#creating-a-virtual-env">creating a virtual env</a></li> + + + + + <li><a href="contributing.html#installing-the-dependencies">installing the dependencies</a></li> + + + + + <li><a href="contributing.html#next-steps">next steps</a></li> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <li class="uk-nav-header">Release Notes</li> + + + + + + <li><a href="NEWS.html#0-7-0--current-">0.7.0 (current)</a></li> + + + + + <li><a href="NEWS.html#0-6-5">0.6.5</a></li> + + + + + <li><a href="NEWS.html#0-6-2">0.6.2</a></li> + + + + + <li><a href="NEWS.html#0-6-1">0.6.1</a></li> + + + + + <li><a href="NEWS.html#0-5-14">0.5.14</a></li> + + + + + <li><a href="NEWS.html#0-5-12">0.5.12</a></li> + + + + + <li><a href="NEWS.html#0-5-11">0.5.11</a></li> + + + + + <li><a href="NEWS.html#0-5-10">0.5.10</a></li> + + + + + <li><a href="NEWS.html#0-5-9">0.5.9</a></li> + + + + + <li><a href="NEWS.html#0-5-8">0.5.8</a></li> + + + + + + + + </ul> + </div> + <div class="uk-width-medium-3-4"> + <article class="uk-article"> + <h1 class="uk-article-title">HTTPretty</h1> + <p class="uk-article-lead">HTTP request mock tool for python</p> + <div class="uk-width-medium-1-1"> + <h1 id="hacking-on-httpretty" name="hacking-on-httpretty"><a href="#hacking-on-httpretty">Hacking on HTTPretty</a></h1><h2 id="creating-a-virtual-env" name="creating-a-virtual-env"><a href="#creating-a-virtual-env">creating a virtual env</a></h2> +<p>you will need <a href="http://www.doughellmann.com/projects/virtualenvwrapper/">virtualenvwrapper</a></p> +<div class="highlight"><pre name="hacking-on-httpretty-example-1"><span class="go">mkvirtualenv --distribute --no-site-packages HTTPretty</span> +</pre></div><h2 id="installing-the-dependencies" name="installing-the-dependencies"><a href="#installing-the-dependencies">installing the dependencies</a></h2><div class="highlight"><pre name="hacking-on-httpretty-example-2"><span class="go">pip install -r requirements.pip</span> +</pre></div><h2 id="next-steps" name="next-steps"><a href="#next-steps">next steps</a></h2> +<ol> +<li>run the tests with make: +<code>bash +make unit functional +</code></li> +<li>hack at will</li> +<li>commit, push etc</li> +<li>send a pull request</li> +</ol> +<h1 id="license" name="license"><a href="#license">License</a></h1><div class="highlight"><pre name="license-example-1"><span class="o"><</span><span class="n">HTTPretty</span> <span class="o">-</span> <span class="n">HTTP</span> <span class="n">client</span> <span class="n">mock</span> <span class="k">for</span> <span class="n">Python</span><span class="o">></span> +<span class="n">Copyright</span> <span class="p">(</span><span class="n">C</span><span class="p">)</span> <span class="o"><</span><span class="mi">2011</span><span class="o">-</span><span class="mi">2013</span><span class="o">></span> <span class="n">Gabriel</span> <span class="n">Falc</span><span class="err">ã</span><span class="n">o</span> <span class="o"><</span><span class="n">gabriel</span><span class="err">@</span><span class="n">nacaolivre</span><span class="p">.</span><span class="n">org</span><span class="o">></span> + +<span class="n">Permission</span> <span class="n">is</span> <span class="n">hereby</span> <span class="n">granted</span><span class="p">,</span> <span class="n">free</span> <span class="n">of</span> <span class="n">charge</span><span class="p">,</span> <span class="n">to</span> <span class="n">any</span> <span class="n">person</span> +<span class="n">obtaining</span> <span class="n">a</span> <span class="n">copy</span> <span class="n">of</span> <span class="n">this</span> <span class="n">software</span> <span class="n">and</span> <span class="n">associated</span> <span class="n">documentation</span> +<span class="n">files</span> <span class="p">(</span><span class="n">the</span> <span class="s">"Software"</span><span class="p">),</span> <span class="n">to</span> <span class="n">deal</span> <span class="n">in</span> <span class="n">the</span> <span class="n">Software</span> <span class="n">without</span> +<span class="n">restriction</span><span class="p">,</span> <span class="n">including</span> <span class="n">without</span> <span class="n">limitation</span> <span class="n">the</span> <span class="n">rights</span> <span class="n">to</span> <span class="n">use</span><span class="p">,</span> +<span class="n">copy</span><span class="p">,</span> <span class="n">modify</span><span class="p">,</span> <span class="n">merge</span><span class="p">,</span> <span class="n">publish</span><span class="p">,</span> <span class="n">distribute</span><span class="p">,</span> <span class="n">sublicense</span><span class="p">,</span> <span class="n">and</span><span class="o">/</span><span class="n">or</span> <span class="n">sell</span> +<span class="n">copies</span> <span class="n">of</span> <span class="n">the</span> <span class="n">Software</span><span class="p">,</span> <span class="n">and</span> <span class="n">to</span> <span class="n">permit</span> <span class="n">persons</span> <span class="n">to</span> <span class="n">whom</span> <span class="n">the</span> +<span class="n">Software</span> <span class="n">is</span> <span class="n">furnished</span> <span class="n">to</span> <span class="k">do</span> <span class="n">so</span><span class="p">,</span> <span class="n">subject</span> <span class="n">to</span> <span class="n">the</span> <span class="n">following</span> +<span class="nl">conditions:</span> + +<span class="n">The</span> <span class="n">above</span> <span class="n">copyright</span> <span class="n">notice</span> <span class="n">and</span> <span class="n">this</span> <span class="n">permission</span> <span class="n">notice</span> <span class="n">shall</span> <span class="n">be</span> +<span class="n">included</span> <span class="n">in</span> <span class="n">all</span> <span class="n">copies</span> <span class="n">or</span> <span class="n">substantial</span> <span class="n">portions</span> <span class="n">of</span> <span class="n">the</span> <span class="n">Software</span><span class="p">.</span> + +<span class="n">THE</span> <span class="n">SOFTWARE</span> <span class="n">IS</span> <span class="n">PROVIDED</span> <span class="s">"AS IS"</span><span class="p">,</span> <span class="n">WITHOUT</span> <span class="n">WARRANTY</span> <span class="n">OF</span> <span class="n">ANY</span> <span class="n">KIND</span><span class="p">,</span> +<span class="n">EXPRESS</span> <span class="n">OR</span> <span class="n">IMPLIED</span><span class="p">,</span> <span class="n">INCLUDING</span> <span class="n">BUT</span> <span class="n">NOT</span> <span class="n">LIMITED</span> <span class="n">TO</span> <span class="n">THE</span> <span class="n">WARRANTIES</span> +<span class="n">OF</span> <span class="n">MERCHANTABILITY</span><span class="p">,</span> <span class="n">FITNESS</span> <span class="n">FOR</span> <span class="n">A</span> <span class="n">PARTICULAR</span> <span class="n">PURPOSE</span> <span class="n">AND</span> +<span class="n">NONINFRINGEMENT</span><span class="p">.</span> <span class="n">IN</span> <span class="n">NO</span> <span class="n">EVENT</span> <span class="n">SHALL</span> <span class="n">THE</span> <span class="n">AUTHORS</span> <span class="n">OR</span> <span class="n">COPYRIGHT</span> +<span class="n">HOLDERS</span> <span class="n">BE</span> <span class="n">LIABLE</span> <span class="n">FOR</span> <span class="n">ANY</span> <span class="n">CLAIM</span><span class="p">,</span> <span class="n">DAMAGES</span> <span class="n">OR</span> <span class="n">OTHER</span> <span class="n">LIABILITY</span><span class="p">,</span> +<span class="n">WHETHER</span> <span class="n">IN</span> <span class="n">AN</span> <span class="n">ACTION</span> <span class="n">OF</span> <span class="n">CONTRACT</span><span class="p">,</span> <span class="n">TORT</span> <span class="n">OR</span> <span class="n">OTHERWISE</span><span class="p">,</span> <span class="n">ARISING</span> +<span class="n">FROM</span><span class="p">,</span> <span class="n">OUT</span> <span class="n">OF</span> <span class="n">OR</span> <span class="n">IN</span> <span class="n">CONNECTION</span> <span class="n">WITH</span> <span class="n">THE</span> <span class="n">SOFTWARE</span> <span class="n">OR</span> <span class="n">THE</span> <span class="n">USE</span> <span class="n">OR</span> +<span class="n">OTHER</span> <span class="n">DEALINGS</span> <span class="n">IN</span> <span class="n">THE</span> <span class="n">SOFTWARE</span><span class="p">.</span> +</pre></div><h1 id="main-contributors" name="main-contributors"><a href="#main-contributors">Main contributors</a></h1> +<p>There folks made remarkable contributions to HTTPretty:</p> + +<ul> +<li>Steve Pulec ~> @spulec</li> +<li>Hugh Saunders ~> @hughsaunders</li> +<li>Matt Luongo ~> @mhluongo</li> +<li>James Rowe ~> @JNRowe</li> +</ul> + + </div> + </div> + </article> + </div> + </div> + </div> + <script type="text/javascript"> + var _gaq = _gaq || []; + _gaq.push(['_setAccount', 'UA-1277640-14']); + _gaq.push(['_trackPageview']); + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); + </script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/jquery-2.0.3.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/angular.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.js'></script> + </body> +</html>
\ No newline at end of file diff --git a/docs.html b/docs.html new file mode 100644 index 0000000..b4d2e1a --- /dev/null +++ b/docs.html @@ -0,0 +1,566 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>HTTPretty by gabrielfalcao</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <!-- Loading Bootstrap --> + + <link rel='stylesheet prefetch' href='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.css'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Montserrat:400,700'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700,900,200italic,300italic,400italic,600italic,700italic,900italic'> + <link rel='stylesheet prefetch' href='//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css'> + <link href="./assets/css/github.css" rel="stylesheet"> + <link href="./assets/css/style.css" rel="stylesheet"> + <script src="./assets/js/prefixfree.min.js"></script> + </head> + <body> + + + <div class="uk-container uk-container-center" style="padding: 120px 0;"> + <div class="uk-grid" data-uk-grid-margin=""> + <div class="uk-width-medium-1-4 uk-hidden-small"> + <div style="clear:both; padding-bottom: 50px;"> + <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/httpretty-logo_1.svg" width="240" title="HTTPretty" alt="HTTPretty" /> + <h2>HTTPretty v0.7.0</h2> + <p> + <iframe src="http://instanc.es/bin/btn/watchers-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/forks-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/follow-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> +</p> + <a class="uk-navbar-brand" href="#">Table of Contents</a> + </div> + + <ul style="clear:both;" class="uk-nav"> + + + + + + + + + <li class="uk-nav-header">What is HTTPretty ?</li> + + + + + + <li><a href="intro.html#a-more-technical-description">A more technical description</a></li> + + + + + <li><a href="intro.html#installing">Installing</a></li> + + + + + + + + + + <li><a href="intro.html#expecting-a-simple-response-body">expecting a simple response body</a></li> + + + + + + + + + + <li><a href="intro.html#the-idea-behind-httpretty--how-it-works-">The idea behind HTTPretty (how it works)</a></li> + + + + + + + + + + + + + + + + <li class="uk-nav-header">Reference</li> + + + + + + <li><a href="docs.html#testing-query-strings">testing query strings</a></li> + + + + + <li><a href="docs.html#using-the-decorator">Using the decorator</a></li> + + + + + <li><a href="docs.html#providing-status-code">Providing status code</a></li> + + + + + <li><a href="docs.html#providing-custom-heades">Providing custom heades</a></li> + + + + + + + + + + <li><a href="docs.html#rotating-responses">rotating responses</a></li> + + + + + <li><a href="docs.html#streaming-responses">streaming responses</a></li> + + + + + <li><a href="docs.html#dynamic-responses-through-callbacks">dynamic responses through callbacks</a></li> + + + + + <li><a href="docs.html#matching-regular-expressions">matching regular expressions</a></li> + + + + + <li><a href="docs.html#expect-for-a-response--and-check-the-request-got-by-the--quot-server-quot--to-make-sure-it-was-fine-">expect for a response, and check the request got by the "server" to make sure it was fine.</a></li> + + + + + <li><a href="docs.html#checking-if-is-enabled">checking if is enabled</a></li> + + + + + + + + + + + + + + <li class="uk-nav-header">Acknowledgements</li> + + + + + + <li><a href="about.html#caveats">caveats</a></li> + + + + + + + + + + <li><a href="about.html#supported-libraries">supported libraries</a></li> + + + + + + + + + + + + + + + + + + + + <li class="uk-nav-header">Hacking on HTTPretty</li> + + + + + + <li><a href="contributing.html#creating-a-virtual-env">creating a virtual env</a></li> + + + + + <li><a href="contributing.html#installing-the-dependencies">installing the dependencies</a></li> + + + + + <li><a href="contributing.html#next-steps">next steps</a></li> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <li class="uk-nav-header">Release Notes</li> + + + + + + <li><a href="NEWS.html#0-7-0--current-">0.7.0 (current)</a></li> + + + + + <li><a href="NEWS.html#0-6-5">0.6.5</a></li> + + + + + <li><a href="NEWS.html#0-6-2">0.6.2</a></li> + + + + + <li><a href="NEWS.html#0-6-1">0.6.1</a></li> + + + + + <li><a href="NEWS.html#0-5-14">0.5.14</a></li> + + + + + <li><a href="NEWS.html#0-5-12">0.5.12</a></li> + + + + + <li><a href="NEWS.html#0-5-11">0.5.11</a></li> + + + + + <li><a href="NEWS.html#0-5-10">0.5.10</a></li> + + + + + <li><a href="NEWS.html#0-5-9">0.5.9</a></li> + + + + + <li><a href="NEWS.html#0-5-8">0.5.8</a></li> + + + + + + + + </ul> + </div> + <div class="uk-width-medium-3-4"> + <article class="uk-article"> + <h1 class="uk-article-title">HTTPretty</h1> + <p class="uk-article-lead">HTTP request mock tool for python</p> + <div class="uk-width-medium-1-1"> + <h1 id="reference" name="reference"><a href="#reference">Reference</a></h1><h2 id="testing-query-strings" name="testing-query-strings"><a href="#testing-query-strings">testing query strings</a></h2><div class="highlight"><pre name="reference-example-1"><span class="kn">import</span> <span class="nn">requests</span> +<span class="kn">from</span> <span class="nn">sure</span> <span class="kn">import</span> <span class="n">expect</span> +<span class="kn">import</span> <span class="nn">httpretty</span> + +<span class="k">def</span> <span class="nf">test_one</span><span class="p">():</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">enable</span><span class="p">()</span> <span class="c"># enable HTTPretty so that it will monkey patch the socket module</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="s">"http://yipit.com/login"</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="s">"Find the best daily deals"</span><span class="p">)</span> + + <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://yipit.com/login?email=user@github.com&password=foobar123'</span><span class="p">)</span> + <span class="n">expect</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">last_request</span><span class="p">())</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">have</span><span class="o">.</span><span class="n">property</span><span class="p">(</span><span class="s">"querystring"</span><span class="p">)</span><span class="o">.</span><span class="n">being</span><span class="o">.</span><span class="n">equal</span><span class="p">({</span> + <span class="s">"email"</span><span class="p">:</span> <span class="s">"user@github.com"</span><span class="p">,</span> + <span class="s">"password"</span><span class="p">:</span> <span class="s">"foobar123"</span><span class="p">,</span> + <span class="p">})</span> + + <span class="n">httpretty</span><span class="o">.</span><span class="n">disable</span><span class="p">()</span> <span class="c"># disable afterwards, so that you will have no problems in code that uses that socket module</span> +</pre></div><h2 id="using-the-decorator" name="using-the-decorator"><a href="#using-the-decorator">Using the decorator</a></h2> +<p><strong>YES</strong> we've got a decorator</p> +<div class="highlight"><pre name="reference-example-2"><span class="kn">import</span> <span class="nn">requests</span> +<span class="kn">import</span> <span class="nn">httpretty</span> + +<span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_one</span><span class="p">():</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="s">"http://yipit.com/"</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="s">"Find the best daily deals"</span><span class="p">)</span> + + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://yipit.com'</span><span class="p">)</span> + <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">text</span> <span class="o">==</span> <span class="s">"Find the best daily deals"</span> +</pre></div> +<p>the <code>@httpretty.activate</code> is a short-hand decorator that wraps the +decorated function with httpretty.enable() and then calls +httpretty.disable() right after.</p> +<h2 id="providing-status-code" name="providing-status-code"><a href="#providing-status-code">Providing status code</a></h2><div class="highlight"><pre name="reference-example-3"><span class="kn">import</span> <span class="nn">requests</span> +<span class="kn">from</span> <span class="nn">sure</span> <span class="kn">import</span> <span class="n">expect</span> +<span class="kn">import</span> <span class="nn">httpretty</span> + +<span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_github_access</span><span class="p">():</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="s">"http://github.com/"</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="s">"here is the mocked body"</span><span class="p">,</span> + <span class="n">status</span><span class="o">=</span><span class="mi">201</span><span class="p">)</span> + + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://github.com'</span><span class="p">)</span> + <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">status_code</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="mi">201</span><span class="p">)</span> +</pre></div><h2 id="providing-custom-heades" name="providing-custom-heades"><a href="#providing-custom-heades">Providing custom heades</a></h2> +<p><strong>and all you need is to add keyword args in which the keys are always lower-cased and with underscores <code>_</code> instead of dashes <code>-</code></strong></p> + +<p>For example, let's say you want to mock that server returns <code>content-type</code>. +To do so, use the argument <code>content_type</code>, <strong>all the keyword args are taken by HTTPretty and transformed in the RFC2616 equivalent name</strong>.</p> +<div class="highlight"><pre name="reference-example-4"><span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_some_api</span><span class="p">():</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="s">"http://foo-api.com/gabrielfalcao"</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="s">'{"success": false}'</span><span class="p">,</span> + <span class="n">status</span><span class="o">=</span><span class="mi">500</span><span class="p">,</span> + <span class="n">content_type</span><span class="o">=</span><span class="s">'text/json'</span><span class="p">)</span> + + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://foo-api.com/gabrielfalcao'</span><span class="p">)</span> + + <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">())</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">({</span><span class="s">'success'</span><span class="p">:</span> <span class="bp">False</span><span class="p">})</span> + <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">status_code</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="mi">500</span><span class="p">)</span> +</pre></div><h3 id="adding-extra-headers-and-forcing-headers" name="adding-extra-headers-and-forcing-headers"><a href="#adding-extra-headers-and-forcing-headers">Adding extra headers and forcing headers</a></h3> +<p>You can pass the <code>adding_headers</code> argument as a dictionary and your +headers will be +<a href="http://en.wikipedia.org/wiki/Union_(set_theory">united</a>) to the +existing headers.</p> +<div class="highlight"><pre name="reference-example-5"><span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_some_api</span><span class="p">():</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="s">"http://foo-api.com/gabrielfalcao"</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="s">'{"success": false}'</span><span class="p">,</span> + <span class="n">status</span><span class="o">=</span><span class="mi">500</span><span class="p">,</span> + <span class="n">content_type</span><span class="o">=</span><span class="s">'text/json'</span><span class="p">,</span> + <span class="n">adding_headers</span><span class="o">=</span><span class="p">{</span> + <span class="s">'X-foo'</span><span class="p">:</span> <span class="s">'bar'</span> + <span class="p">})</span> + + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://foo-api.com/gabrielfalcao'</span><span class="p">)</span> + + <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">())</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">({</span><span class="s">'success'</span><span class="p">:</span> <span class="bp">False</span><span class="p">})</span> + <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">status_code</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="mi">500</span><span class="p">)</span> +</pre></div> +<p>Although there are some situation where some headers line +<code>content-length</code> will be calculated by HTTPretty based on the +specified fake response body.</p> + +<p>So you might want to <em>“force”</em> those headers:</p> +<div class="highlight"><pre name="reference-example-6"><span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_some_api</span><span class="p">():</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="s">"http://foo-api.com/gabrielfalcao"</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="s">'{"success": false}'</span><span class="p">,</span> + <span class="n">status</span><span class="o">=</span><span class="mi">500</span><span class="p">,</span> + <span class="n">content_type</span><span class="o">=</span><span class="s">'text/json'</span><span class="p">,</span> + <span class="n">forcing_headers</span><span class="o">=</span><span class="p">{</span> + <span class="s">'content-length'</span><span class="p">:</span> <span class="s">'100'</span> + <span class="p">})</span> + + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://foo-api.com/gabrielfalcao'</span><span class="p">)</span> + + <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">())</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">({</span><span class="s">'success'</span><span class="p">:</span> <span class="bp">False</span><span class="p">})</span> + <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">status_code</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="mi">500</span><span class="p">)</span> +</pre></div> +<p>You should, though, be careful with it. The HTTP client is likely to +rely on the content length to know how many bytes of response payload +should be loaded. Forcing a <code>content-length</code> that is bigger than the +action response body might cause the HTTP client to hang because it is +waiting for data. Read more in the “caveats” session on the bottom.</p> +<h2 id="rotating-responses" name="rotating-responses"><a href="#rotating-responses">rotating responses</a></h2> +<p>Same URL, same request method, the first request return the first +httpretty.Response, all the subsequent ones return the last (status 202).</p> + +<p>Notice that the <code>responses</code> argument is a list and you can pass as +many responses as you want.</p> +<div class="highlight"><pre name="reference-example-7"><span class="kn">import</span> <span class="nn">requests</span> +<span class="kn">from</span> <span class="nn">sure</span> <span class="kn">import</span> <span class="n">expect</span> + + +<span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_rotating_responses</span><span class="p">():</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="s">"http://github.com/gabrielfalcao/httpretty"</span><span class="p">,</span> + <span class="n">responses</span><span class="o">=</span><span class="p">[</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">Response</span><span class="p">(</span><span class="n">body</span><span class="o">=</span><span class="s">"first response"</span><span class="p">,</span> <span class="n">status</span><span class="o">=</span><span class="mi">201</span><span class="p">),</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">Response</span><span class="p">(</span><span class="n">body</span><span class="o">=</span><span class="s">'second and last response'</span><span class="p">,</span> <span class="n">status</span><span class="o">=</span><span class="mi">202</span><span class="p">),</span> + <span class="p">])</span> + + <span class="n">response1</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://github.com/gabrielfalcao/httpretty'</span><span class="p">)</span> + <span class="n">expect</span><span class="p">(</span><span class="n">response1</span><span class="o">.</span><span class="n">status_code</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="mi">201</span><span class="p">)</span> + <span class="n">expect</span><span class="p">(</span><span class="n">response1</span><span class="o">.</span><span class="n">text</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="s">'first response'</span><span class="p">)</span> + + <span class="n">response2</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://github.com/gabrielfalcao/httpretty'</span><span class="p">)</span> + <span class="n">expect</span><span class="p">(</span><span class="n">response2</span><span class="o">.</span><span class="n">status_code</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="mi">202</span><span class="p">)</span> + <span class="n">expect</span><span class="p">(</span><span class="n">response2</span><span class="o">.</span><span class="n">text</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="s">'second and last response'</span><span class="p">)</span> + + <span class="n">response3</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://github.com/gabrielfalcao/httpretty'</span><span class="p">)</span> + + <span class="n">expect</span><span class="p">(</span><span class="n">response3</span><span class="o">.</span><span class="n">status_code</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="mi">202</span><span class="p">)</span> + <span class="n">expect</span><span class="p">(</span><span class="n">response3</span><span class="o">.</span><span class="n">text</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="s">'second and last response'</span><span class="p">)</span> +</pre></div><h2 id="streaming-responses" name="streaming-responses"><a href="#streaming-responses">streaming responses</a></h2> +<p>Mock a streaming response by registering a generator response body.</p> +<div class="highlight"><pre name="reference-example-8"><span class="kn">import</span> <span class="nn">requests</span> +<span class="kn">from</span> <span class="nn">sure</span> <span class="kn">import</span> <span class="n">expect</span> +<span class="kn">import</span> <span class="nn">httpretty</span> + +<span class="c"># mock a streaming response body with a generator</span> +<span class="k">def</span> <span class="nf">mock_streaming_tweets</span><span class="p">(</span><span class="n">tweets</span><span class="p">):</span> + <span class="kn">from</span> <span class="nn">time</span> <span class="kn">import</span> <span class="n">sleep</span> + <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">tweets</span><span class="p">:</span> + <span class="n">sleep</span><span class="p">(</span><span class="o">.</span><span class="mi">5</span><span class="p">)</span> + <span class="k">yield</span> <span class="n">t</span> + +<span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_twitter_api_integration</span><span class="p">(</span><span class="n">now</span><span class="p">):</span> + <span class="n">twitter_response_lines</span> <span class="o">=</span> <span class="p">[</span> + <span class="s">'{"text":"If @BarackObama requests to follow me one more time I</span><span class="se">\'</span><span class="s">m calling the police."}</span><span class="se">\r\n</span><span class="s">'</span><span class="p">,</span> + <span class="s">'</span><span class="se">\r\n</span><span class="s">'</span><span class="p">,</span> + <span class="s">'{"text":"Thanks for all your #FollowMe1D requests Directioners! We</span><span class="se">\u2019</span><span class="s">ll be following 10 people throughout the day starting NOW. G ..."}</span><span class="se">\r\n</span><span class="s">'</span> + <span class="p">]</span> + + <span class="n">TWITTER_STREAMING_URL</span> <span class="o">=</span> <span class="s">"https://stream.twitter.com/1/statuses/filter.json"</span> + + <span class="c"># set the body to a generator and set `streaming=True` to mock a streaming response body</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">POST</span><span class="p">,</span> <span class="n">TWITTER_STREAMING_URL</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="n">mock_streaming_tweets</span><span class="p">(</span><span class="n">twitter_response_lines</span><span class="p">),</span> + <span class="n">streaming</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> + + <span class="c"># taken from the requests docs</span> + <span class="c"># http://docs.python-requests.org/en/latest/user/advanced/#streaming-requests</span> + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">TWITTER_STREAMING_URL</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="p">{</span><span class="s">'track'</span><span class="p">:</span><span class="s">'requests'</span><span class="p">},</span> + <span class="n">auth</span><span class="o">=</span><span class="p">(</span><span class="s">'username'</span><span class="p">,</span><span class="s">'password'</span><span class="p">),</span> <span class="n">prefetch</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> + + <span class="c">#test iterating by line</span> + <span class="n">line_iter</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">iter_lines</span><span class="p">()</span> + <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">twitter_response_lines</span><span class="p">)):</span> + <span class="n">expect</span><span class="p">(</span><span class="n">line_iter</span><span class="o">.</span><span class="n">next</span><span class="p">()</span><span class="o">.</span><span class="n">strip</span><span class="p">())</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="n">twitter_response_lines</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="o">.</span><span class="n">strip</span><span class="p">())</span> +</pre></div><h2 id="dynamic-responses-through-callbacks" name="dynamic-responses-through-callbacks"><a href="#dynamic-responses-through-callbacks">dynamic responses through callbacks</a></h2> +<p>Set a callback to allow for dynamic responses based on the request.</p> +<div class="highlight"><pre name="reference-example-9"><span class="kn">import</span> <span class="nn">requests</span> +<span class="kn">from</span> <span class="nn">sure</span> <span class="kn">import</span> <span class="n">expect</span> +<span class="kn">import</span> <span class="nn">httpretty</span> + +<span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_response_callbacks</span><span class="p">():</span> + + <span class="k">def</span> <span class="nf">request_callback</span><span class="p">(</span><span class="n">method</span><span class="p">,</span> <span class="n">uri</span><span class="p">,</span> <span class="n">headers</span><span class="p">):</span> + <span class="k">return</span> <span class="p">(</span><span class="mi">200</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="s">"The {} response from {}"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">method</span><span class="p">,</span> <span class="n">uri</span><span class="p">))</span> + + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="s">"https://api.yahoo.com/test"</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="n">request_callback</span><span class="p">)</span> + + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'https://api.yahoo.com/test'</span><span class="p">)</span> + + <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="s">'The GET response from https://api.yahoo.com/test'</span><span class="p">)</span> +</pre></div><h2 id="matching-regular-expressions" name="matching-regular-expressions"><a href="#matching-regular-expressions">matching regular expressions</a></h2> +<p>You can register a +<a href="http://docs.python.org/2/library/re.html#re.compile">compiled regex</a> +and it will be matched against the requested urls.</p> +<div class="highlight"><pre name="reference-example-10"><span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_httpretty_should_allow_registering_regexes</span><span class="p">():</span> + <span class="s">u"HTTPretty should allow registering regexes"</span> + + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> + <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="s">"api.yipit.com/v2/deal;brand=(\w+)"</span><span class="p">),</span> + <span class="n">body</span><span class="o">=</span><span class="s">"Found brand"</span><span class="p">,</span> + <span class="p">)</span> + + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'https://api.yipit.com/v2/deal;brand=GAP'</span><span class="p">)</span> + <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="s">'Found brand'</span><span class="p">)</span> + <span class="n">expect</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">last_request</span><span class="p">()</span><span class="o">.</span><span class="n">method</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="s">'GET'</span><span class="p">)</span> + <span class="n">expect</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">last_request</span><span class="p">()</span><span class="o">.</span><span class="n">path</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="s">'/v1/deal;brand=GAP'</span><span class="p">)</span> +</pre></div> +<p>By default, the regexp you register will match the requests without looking at +the querystring. If you want the querystring to be considered, you can set +<code>match_querystring=True</code> when calling <code>register_uri</code>.</p> +<h2 id="expect-for-a-response--and-check-the-request-got-by-the--quot-server-quot--to-make-sure-it-was-fine-" name="expect-for-a-response--and-check-the-request-got-by-the--quot-server-quot--to-make-sure-it-was-fine-"><a href="#expect-for-a-response--and-check-the-request-got-by-the--quot-server-quot--to-make-sure-it-was-fine-">expect for a response, and check the request got by the “server” to make sure it was fine.</a></h2><div class="highlight"><pre name="reference-example-11"><span class="kn">import</span> <span class="nn">requests</span> +<span class="kn">from</span> <span class="nn">sure</span> <span class="kn">import</span> <span class="n">expect</span> +<span class="kn">import</span> <span class="nn">httpretty</span> + + +<span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_yipit_api_integration</span><span class="p">():</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">POST</span><span class="p">,</span> <span class="s">"http://api.yipit.com/foo/"</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="s">'{"repositories": ["HTTPretty", "lettuce"]}'</span><span class="p">)</span> + + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s">'http://api.yipit.com/foo'</span><span class="p">,</span> + <span class="s">'{"username": "gabrielfalcao"}'</span><span class="p">,</span> + <span class="n">headers</span><span class="o">=</span><span class="p">{</span> + <span class="s">'content-type'</span><span class="p">:</span> <span class="s">'text/json'</span><span class="p">,</span> + <span class="p">})</span> + + <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="s">'{"repositories": ["HTTPretty", "lettuce"]}'</span><span class="p">)</span> + <span class="n">expect</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">last_request</span><span class="p">()</span><span class="o">.</span><span class="n">method</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="s">"POST"</span><span class="p">)</span> + <span class="n">expect</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">last_request</span><span class="p">()</span><span class="o">.</span><span class="n">headers</span><span class="p">[</span><span class="s">'content-type'</span><span class="p">])</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">(</span><span class="s">'text/json'</span><span class="p">)</span> +</pre></div><h2 id="checking-if-is-enabled" name="checking-if-is-enabled"><a href="#checking-if-is-enabled">checking if is enabled</a></h2><div class="highlight"><pre name="reference-example-12"><span class="n">httpretty</span><span class="o">.</span><span class="n">enable</span><span class="p">()</span> +<span class="n">httpretty</span><span class="o">.</span><span class="n">is_enabled</span><span class="p">()</span><span class="o">.</span><span class="n">should</span><span class="o">.</span><span class="n">be</span><span class="o">.</span><span class="n">true</span> + +<span class="n">httpretty</span><span class="o">.</span><span class="n">disable</span><span class="p">()</span> +<span class="n">httpretty</span><span class="o">.</span><span class="n">is_enabled</span><span class="p">()</span><span class="o">.</span><span class="n">should</span><span class="o">.</span><span class="n">be</span><span class="o">.</span><span class="n">false</span> +</pre></div> + </div> + </div> + </article> + </div> + </div> + </div> + <script type="text/javascript"> + var _gaq = _gaq || []; + _gaq.push(['_setAccount', 'UA-1277640-14']); + _gaq.push(['_trackPageview']); + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); + </script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/jquery-2.0.3.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/angular.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.js'></script> + </body> +</html>
\ No newline at end of file diff --git a/docs/.markment.yml b/docs/.markment.yml new file mode 100644 index 0000000..f669aba --- /dev/null +++ b/docs/.markment.yml @@ -0,0 +1,23 @@ +project: + name: "HTTPretty" + version: 0.7.0 + description: HTTP request mock tool for python + tagline: Intercept real HTTP calls in python, test your software better. + twitter: gabrielfalcao + twitter_message: Python%20library%20for%20intercepting%20and%20mocking%20HTTP%20calls + google_analytics_code: UA-1277640-14 + github_maintainer: gabrielfalcao + github_maintainer_name: Gabriel Falcão + github_maintainer_url: http://github.com/gabrielfalcao + github_maintainer_gravatar: https://secure.gravatar.com/avatar/3fa0df5c54f5ac0f8652d992d7d24039?s=64 + + github_url: http://github.com/gabrielfalcao/HTTPretty + github_fork_url: https://github.com/gabrielfalcao/HTTPretty/archive/master.tar.gz + tarball_download_url: https://github.com/gabrielfalcao/HTTPretty/archive/master.tar.gz + zipball_download_url: https://github.com/gabrielfalcao/HTTPretty/archive/master.zip + documentation_url: http://falcao.it/HTTPretty + support_url: http://github.com/gabrielfalcao/HTTPretty/issues + logo_url: https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/httpretty-logo_1.svg + full_index: false +documentation: + index: intro.md diff --git a/docs/NEWS.md b/docs/NEWS.md new file mode 100644 index 0000000..d8fb0e1 --- /dev/null +++ b/docs/NEWS.md @@ -0,0 +1,80 @@ +# Release Notes + +## 0.7.0 (current) + +Improvements: + +* Refactored `core.py` and increased its unit test coverage to 80%. HTTPretty is slightly more robust now. + +Bug fixes: + +* POST requests being called twice [#100](https://github.com/gabrielfalcao/HTTPretty/pull/100) + +## 0.6.5 + +Applied pull requests: + +* continue on EAGAIN socket errors: [#102](https://github.com/gabrielfalcao/HTTPretty/pull/102) by [kouk](http://github.com/kouk). +* Fix `fake_gethostbyname` for requests 2.0: [#101](https://github.com/gabrielfalcao/HTTPretty/pull/101) by [mgood](http://github.com/mgood) +* Add a way to match the querystrings: [#98](https://github.com/gabrielfalcao/HTTPretty/pull/98) by [ametaireau](http://github.com/ametaireau) +* Use common string case for URIInfo hostname comparison: [#95](https://github.com/gabrielfalcao/HTTPretty/pull/95) by [mikewaters](http://github.com/mikewaters) +* Expose httpretty.reset() to public API: [#91](https://github.com/gabrielfalcao/HTTPretty/pull/91) by [imankulov](http://github.com/imankulov) +* Don't duplicate http ports number: [#89](https://github.com/gabrielfalcao/HTTPretty/pull/89) by [mardiros](http://github.com/mardiros) +* Adding parsed_body parameter to simplify checks: [#88](https://github.com/gabrielfalcao/HTTPretty/pull/88) by [toumorokoshi](http://github.com/toumorokoshi) +* Use the real socket if it's not HTTP: [#87](https://github.com/gabrielfalcao/HTTPretty/pull/87) by [mardiros](http://github.com/mardiros) + +## 0.6.2 + +* Fixing bug of lack of trailing slashes [#73](https://github.com/gabrielfalcao/HTTPretty/issues/73) +* Applied pull requests [#71](https://github.com/gabrielfalcao/HTTPretty/pull/71) and [#72](https://github.com/gabrielfalcao/HTTPretty/pull/72) by @andresriancho +* Keyword arg coercion fix by @dupuy +* @papaeye fixed content-length calculation. + +## 0.6.1 + +* New API, no more camel case and everything is available through a simple import: + +```python +import httpretty + +@httpretty.activate +def test_function(): + # httpretty.register_uri(...) + # make request... + pass +``` + +* Re-organized module into submodules + +## 0.5.14 + +* Delegate calls to other methods on socket + +* [Normalized header](https://github.com/gabrielfalcao/HTTPretty/pull/49) strings + +* Callbacks are [more intelligent now](https://github.com/gabrielfalcao/HTTPretty/pull/47) + +* Normalize urls matching for url quoting + +## 0.5.12 + +* HTTPretty doesn't hang when using other application protocols under + a @httprettified decorated test. + +## 0.5.11 + +* Ability to know whether HTTPretty is or not enabled through `httpretty.is_enabled()` + +## 0.5.10 + +* Support to multiple methods per registered URL. Thanks @hughsaunders + +## 0.5.9 + +* Fixed python 3 support. Thanks @spulec + +## 0.5.8 + +* Support to [register regular expressions to match urls](#matching-regular-expressions) +* [Body callback](#dynamic-responses-through-callbacks) suppport +* Python 3 support diff --git a/docs/about.md b/docs/about.md new file mode 100644 index 0000000..44b87a7 --- /dev/null +++ b/docs/about.md @@ -0,0 +1,18 @@ +# Acknowledgements + +## caveats + +### `forcing_headers` + `Content-Length` + +if you use the `forcing_headers` options make sure to add the header +`Content-Length` otherwise the +[requests](http://docs.python-requests.org/en/latest/) will try to +load the response endlessly + +## supported libraries + +Because HTTPretty works in the socket level it should work with any HTTP client libraries, although it is [battle tested](https://github.com/gabrielfalcao/HTTPretty/tree/master/tests/functional) against: + +* [requests](http://docs.python-requests.org/en/latest/) +* [httplib2](http://code.google.com/p/httplib2/) +* [urllib2](http://docs.python.org/2/library/urllib2.html) diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..e4ed3ae --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,62 @@ +# Hacking on HTTPretty + +## creating a virtual env + +you will need [virtualenvwrapper](http://www.doughellmann.com/projects/virtualenvwrapper/) + + +```console +mkvirtualenv --distribute --no-site-packages HTTPretty +``` + +## installing the dependencies + +```console +pip install -r requirements.pip +``` + +## next steps + +1. run the tests with make: +```bash +make unit functional +``` +2. hack at will +3. commit, push etc +4. send a pull request + +# License + + <HTTPretty - HTTP client mock for Python> + Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + +# Main contributors + +There folks made remarkable contributions to HTTPretty: + +* Steve Pulec ~> @spulec +* Hugh Saunders ~> @hughsaunders +* Matt Luongo ~> @mhluongo +* James Rowe ~> @JNRowe diff --git a/docs/docs.md b/docs/docs.md new file mode 100644 index 0000000..c87f401 --- /dev/null +++ b/docs/docs.md @@ -0,0 +1,299 @@ +# Reference + +## testing query strings + +```python +import requests +from sure import expect +import httpretty + +def test_one(): + httpretty.enable() # enable HTTPretty so that it will monkey patch the socket module + httpretty.register_uri(httpretty.GET, "http://yipit.com/login", + body="Find the best daily deals") + + requests.get('http://yipit.com/login?email=user@github.com&password=foobar123') + expect(httpretty.last_request()).to.have.property("querystring").being.equal({ + "email": "user@github.com", + "password": "foobar123", + }) + + httpretty.disable() # disable afterwards, so that you will have no problems in code that uses that socket module +``` + + +## Using the decorator + +**YES** we've got a decorator + +```python +import requests +import httpretty + +@httpretty.activate +def test_one(): + httpretty.register_uri(httpretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + response = requests.get('http://yipit.com') + assert response.text == "Find the best daily deals" +``` + +the `@httpretty.activate` is a short-hand decorator that wraps the +decorated function with httpretty.enable() and then calls +httpretty.disable() right after. + +## Providing status code + +```python +import requests +from sure import expect +import httpretty + +@httpretty.activate +def test_github_access(): + httpretty.register_uri(httpretty.GET, "http://github.com/", + body="here is the mocked body", + status=201) + + response = requests.get('http://github.com') + expect(response.status_code).to.equal(201) +``` + +## Providing custom heades + +**and all you need is to add keyword args in which the keys are always lower-cased and with underscores `_` instead of dashes `-`** + +For example, let's say you want to mock that server returns `content-type`. +To do so, use the argument `content_type`, **all the keyword args are taken by HTTPretty and transformed in the RFC2616 equivalent name**. + +```python +@httpretty.activate +def test_some_api(): + httpretty.register_uri(httpretty.GET, "http://foo-api.com/gabrielfalcao", + body='{"success": false}', + status=500, + content_type='text/json') + + response = requests.get('http://foo-api.com/gabrielfalcao') + + expect(response.json()).to.equal({'success': False}) + expect(response.status_code).to.equal(500) +``` + + +### Adding extra headers and forcing headers + +You can pass the `adding_headers` argument as a dictionary and your +headers will be +[united](http://en.wikipedia.org/wiki/Union_(set_theory)) to the +existing headers. + +```python +@httpretty.activate +def test_some_api(): + httpretty.register_uri(httpretty.GET, "http://foo-api.com/gabrielfalcao", + body='{"success": false}', + status=500, + content_type='text/json', + adding_headers={ + 'X-foo': 'bar' + }) + + response = requests.get('http://foo-api.com/gabrielfalcao') + + expect(response.json()).to.equal({'success': False}) + expect(response.status_code).to.equal(500) +``` + +Although there are some situation where some headers line +`content-length` will be calculated by HTTPretty based on the +specified fake response body. + +So you might want to *"force"* those headers: + +```python +@httpretty.activate +def test_some_api(): + httpretty.register_uri(httpretty.GET, "http://foo-api.com/gabrielfalcao", + body='{"success": false}', + status=500, + content_type='text/json', + forcing_headers={ + 'content-length': '100' + }) + + response = requests.get('http://foo-api.com/gabrielfalcao') + + expect(response.json()).to.equal({'success': False}) + expect(response.status_code).to.equal(500) +``` + +You should, though, be careful with it. The HTTP client is likely to +rely on the content length to know how many bytes of response payload +should be loaded. Forcing a `content-length` that is bigger than the +action response body might cause the HTTP client to hang because it is +waiting for data. Read more in the "caveats" session on the bottom. + +## rotating responses + +Same URL, same request method, the first request return the first +httpretty.Response, all the subsequent ones return the last (status 202). + +Notice that the `responses` argument is a list and you can pass as +many responses as you want. + +```python +import requests +from sure import expect + + +@httpretty.activate +def test_rotating_responses(): + httpretty.register_uri(httpretty.GET, "http://github.com/gabrielfalcao/httpretty", + responses=[ + httpretty.Response(body="first response", status=201), + httpretty.Response(body='second and last response', status=202), + ]) + + response1 = requests.get('http://github.com/gabrielfalcao/httpretty') + expect(response1.status_code).to.equal(201) + expect(response1.text).to.equal('first response') + + response2 = requests.get('http://github.com/gabrielfalcao/httpretty') + expect(response2.status_code).to.equal(202) + expect(response2.text).to.equal('second and last response') + + response3 = requests.get('http://github.com/gabrielfalcao/httpretty') + + expect(response3.status_code).to.equal(202) + expect(response3.text).to.equal('second and last response') +``` +## streaming responses + +Mock a streaming response by registering a generator response body. + +```python +import requests +from sure import expect +import httpretty + +# mock a streaming response body with a generator +def mock_streaming_tweets(tweets): + from time import sleep + for t in tweets: + sleep(.5) + yield t + +@httpretty.activate +def test_twitter_api_integration(now): + twitter_response_lines = [ + '{"text":"If @BarackObama requests to follow me one more time I\'m calling the police."}\r\n', + '\r\n', + '{"text":"Thanks for all your #FollowMe1D requests Directioners! We\u2019ll be following 10 people throughout the day starting NOW. G ..."}\r\n' + ] + + TWITTER_STREAMING_URL = "https://stream.twitter.com/1/statuses/filter.json" + + # set the body to a generator and set `streaming=True` to mock a streaming response body + httpretty.register_uri(httpretty.POST, TWITTER_STREAMING_URL, + body=mock_streaming_tweets(twitter_response_lines), + streaming=True) + + # taken from the requests docs + # http://docs.python-requests.org/en/latest/user/advanced/#streaming-requests + response = requests.post(TWITTER_STREAMING_URL, data={'track':'requests'}, + auth=('username','password'), prefetch=False) + + #test iterating by line + line_iter = response.iter_lines() + for i in xrange(len(twitter_response_lines)): + expect(line_iter.next().strip()).to.equal(twitter_response_lines[i].strip()) +``` + +## dynamic responses through callbacks + +Set a callback to allow for dynamic responses based on the request. + +```python +import requests +from sure import expect +import httpretty + +@httpretty.activate +def test_response_callbacks(): + + def request_callback(method, uri, headers): + return (200, headers, "The {} response from {}".format(method, uri)) + + httpretty.register_uri( + httpretty.GET, "https://api.yahoo.com/test", + body=request_callback) + + response = requests.get('https://api.yahoo.com/test') + + expect(response.text).to.equal('The GET response from https://api.yahoo.com/test') +``` + +## matching regular expressions + +You can register a +[compiled regex](http://docs.python.org/2/library/re.html#re.compile) +and it will be matched against the requested urls. + +```python +@httpretty.activate +def test_httpretty_should_allow_registering_regexes(): + u"HTTPretty should allow registering regexes" + + httpretty.register_uri( + httpretty.GET, + re.compile("api.yipit.com/v2/deal;brand=(\w+)"), + body="Found brand", + ) + + response = requests.get('https://api.yipit.com/v2/deal;brand=GAP') + expect(response.text).to.equal('Found brand') + expect(httpretty.last_request().method).to.equal('GET') + expect(httpretty.last_request().path).to.equal('/v1/deal;brand=GAP') +``` + +By default, the regexp you register will match the requests without looking at +the querystring. If you want the querystring to be considered, you can set +`match_querystring=True` when calling `register_uri`. + +## expect for a response, and check the request got by the "server" to make sure it was fine. + +```python +import requests +from sure import expect +import httpretty + + +@httpretty.activate +def test_yipit_api_integration(): + httpretty.register_uri(httpretty.POST, "http://api.yipit.com/foo/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + response = requests.post('http://api.yipit.com/foo', + '{"username": "gabrielfalcao"}', + headers={ + 'content-type': 'text/json', + }) + + expect(response.text).to.equal('{"repositories": ["HTTPretty", "lettuce"]}') + expect(httpretty.last_request().method).to.equal("POST") + expect(httpretty.last_request().headers['content-type']).to.equal('text/json') +``` + +## checking if is enabled + +```python + +httpretty.enable() +httpretty.is_enabled().should.be.true + +httpretty.disable() +httpretty.is_enabled().should.be.false + +``` diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 0000000..7187a65 --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,82 @@ +# What is HTTPretty ? + +Once upon a time a python developer wanted to use a RESTful api, +everything was fine but until the day he needed to test the code that +hits the RESTful API: what if the API server is down? What if its +content has changed ? + +Don't worry, HTTPretty is here for you: + +```python +import requests +from sure import expect +import httpretty + + +@httpretty.activate +def test_yipit_api_returning_deals(): + httpretty.register_uri(httpretty.GET, "http://api.yipit.com/v1/deals/", + body='[{"title": "Test Deal"}]', + content_type="application/json") + + response = requests.get('http://api.yipit.com/v1/deals/') + + expect(response.json()).to.equal([{"title": "Test Deal"}]) +``` + +## A more technical description + +HTTPretty is a HTTP client mock library for Python 100% inspired on ruby's [FakeWeb](http://fakeweb.rubyforge.org/). +If you come from ruby this would probably sound familiar :smiley: + +## Installing + +Installing httpretty is as easy as: + +```bash +pip install HTTPretty +``` + +# Demo + +## expecting a simple response body + +```python +import requests +import httpretty + +def test_one(): + httpretty.enable() # enable HTTPretty so that it will monkey patch the socket module + httpretty.register_uri(httpretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + response = requests.get('http://yipit.com') + + assert response.text == "Find the best daily deals" + + httpretty.disable() # disable afterwards, so that you will have no problems in code that uses that socket module + httpretty.reset() # reset HTTPretty state (clean up registered urls and request history) +``` + +# Motivation + +When building systems that access external resources such as RESTful +webservices, XMLRPC or even simple HTTP requests, we stumble in the +problem: + + "I'm gonna need to mock all those requests" + +It brings a lot of hassle, you will need to use a generic mocking +tool, mess with scope and so on. + +## The idea behind HTTPretty (how it works) + +HTTPretty [monkey patches](http://en.wikipedia.org/wiki/Monkey_patch) +Python's [socket](http://docs.python.org/library/socket.html) core +module, reimplementing the HTTP protocol, by mocking requests and +responses. + +As for it works in this way, you don't need to worry what http library +you're gonna use. + +HTTPretty will mock the response for you :) *(and also give you the latest requests so that you can check them)* diff --git a/httpretty/__init__.py b/httpretty/__init__.py new file mode 100644 index 0000000..1d7b8a6 --- /dev/null +++ b/httpretty/__init__.py @@ -0,0 +1,47 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +__version__ = version = '0.7.0' + +import sys + +from .core import httpretty, httprettified +from .errors import HTTPrettyError +from .core import URIInfo + +HTTPretty = httpretty +activate = httprettified + +SELF = sys.modules[__name__] + +for attr in [name.decode() for name in httpretty.METHODS] + ['register_uri', 'enable', 'disable', 'is_enabled', 'reset', 'Response']: + setattr(SELF, attr, getattr(httpretty, attr)) + + +def last_request(): + """returns the last request""" + return httpretty.last_request diff --git a/httpretty/compat.py b/httpretty/compat.py new file mode 100644 index 0000000..41c909c --- /dev/null +++ b/httpretty/compat.py @@ -0,0 +1,90 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +import sys +import types + +PY3 = sys.version_info[0] == 3 +if PY3: # pragma: no cover + text_type = str + byte_type = bytes + import io + StringIO = io.BytesIO + basestring = (str, bytes) + + class BaseClass(object): + def __repr__(self): + return self.__str__() +else: # pragma: no cover + text_type = unicode + byte_type = str + import StringIO + StringIO = StringIO.StringIO + basestring = basestring + + +class BaseClass(object): + def __repr__(self): + ret = self.__str__() + if PY3: # pragma: no cover + return ret + else: + return ret.encode('utf-8') + + +try: # pragma: no cover + from urllib.parse import urlsplit, urlunsplit, parse_qs, quote, quote_plus, unquote +except ImportError: # pragma: no cover + from urlparse import urlsplit, urlunsplit, parse_qs, unquote + from urllib import quote, quote_plus + +try: # pragma: no cover + from http.server import BaseHTTPRequestHandler +except ImportError: # pragma: no cover + from BaseHTTPServer import BaseHTTPRequestHandler + + +ClassTypes = (type,) +if not PY3: # pragma: no cover + ClassTypes = (type, types.ClassType) + + +__all__ = [ + 'PY3', + 'StringIO', + 'text_type', + 'byte_type', + 'BaseClass', + 'BaseHTTPRequestHandler', + 'quote', + 'quote_plus', + 'urlunsplit', + 'urlsplit', + 'parse_qs', + 'ClassTypes', +] diff --git a/httpretty/core.py b/httpretty/core.py new file mode 100644 index 0000000..ddb1b1d --- /dev/null +++ b/httpretty/core.py @@ -0,0 +1,1013 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +import re +import codecs +import inspect +import socket +import functools +import itertools +import warnings +import logging +import traceback +import json +import contextlib + + +from .compat import ( + PY3, + StringIO, + text_type, + BaseClass, + BaseHTTPRequestHandler, + quote, + quote_plus, + urlunsplit, + urlsplit, + parse_qs, + unquote, + ClassTypes, + basestring +) +from .http import ( + STATUSES, + HttpBaseClass, + parse_requestline, + last_requestline, +) + +from .utils import ( + utf8, + decode_utf8, +) + +from .errors import HTTPrettyError + +from datetime import datetime +from datetime import timedelta +from errno import EAGAIN + +old_socket = socket.socket +old_create_connection = socket.create_connection +old_gethostbyname = socket.gethostbyname +old_gethostname = socket.gethostname +old_getaddrinfo = socket.getaddrinfo +old_socksocket = None +old_ssl_wrap_socket = None +old_sslwrap_simple = None +old_sslsocket = None + +if PY3: # pragma: no cover + basestring = (bytes, str) +try: # pragma: no cover + import socks + old_socksocket = socks.socksocket +except ImportError: + socks = None + +try: # pragma: no cover + import ssl + old_ssl_wrap_socket = ssl.wrap_socket + if not PY3: + old_sslwrap_simple = ssl.sslwrap_simple + old_sslsocket = ssl.SSLSocket +except ImportError: # pragma: no cover + ssl = None + + +POTENTIAL_HTTP_PORTS = set([80, 443]) +DEFAULT_HTTP_PORTS = tuple(POTENTIAL_HTTP_PORTS) + + +class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): + """Represents a HTTP request. It takes a valid multi-line, `\r\n` + separated string with HTTP headers and parse them out using the + internal `parse_request` method. + + It also replaces the `rfile` and `wfile` attributes with StringIO + instances so that we garantee that it won't make any I/O, neighter + for writing nor reading. + + It has some convenience attributes: + + `headers` -> a mimetype object that can be cast into a dictionary, + contains all the request headers + + `method` -> the HTTP method used in this request + + `querystring` -> a dictionary containing lists with the + attributes. Please notice that if you need a single value from a + query string you will need to get it manually like: + + ```python + >>> request.querystring + {'name': ['Gabriel Falcao']} + >>> print request.querystring['name'][0] + ``` + + `parsed_body` -> a dictionary containing parsed request body or + None if HTTPrettyRequest doesn't know how to parse it. It + currently supports parsing body data that was sent under the + `content-type` headers values: 'application/json' or + 'application/x-www-form-urlencoded' + """ + def __init__(self, headers, body=''): + # first of all, lets make sure that if headers or body are + # unicode strings, it must be converted into a utf-8 encoded + # byte string + self.raw_headers = utf8(headers.strip()) + self.body = utf8(body) + + # Now let's concatenate the headers with the body, and create + # `rfile` based on it + self.rfile = StringIO(b'\r\n\r\n'.join([self.raw_headers, self.body])) + self.wfile = StringIO() # Creating `wfile` as an empty + # StringIO, just to avoid any real + # I/O calls + + # parsing the request line preemptively + self.raw_requestline = self.rfile.readline() + + # initiating the error attributes with None + self.error_code = None + self.error_message = None + + # Parse the request based on the attributes above + self.parse_request() + + # making the HTTP method string available as the command + self.method = self.command + + # Now 2 convenient attributes for the HTTPretty API: + + # `querystring` holds a dictionary with the parsed query string + self.path = decode_utf8(self.path) + + qstring = self.path.split("?", 1)[-1] + self.querystring = self.parse_querystring(qstring) + + # And the body will be attempted to be parsed as + # `application/json` or `application/x-www-form-urlencoded` + self.parsed_body = self.parse_request_body(self.body) + + def __str__(self): + return '<HTTPrettyRequest("{0}", total_headers={1}, body_length={2})>'.format( + self.headers.get('content-type', ''), + len(self.headers), + len(self.body), + ) + + def parse_querystring(self, qs): + expanded = decode_utf8(unquote(utf8(qs))) + + parsed = parse_qs(expanded) + result = {} + for k, v in parsed.iteritems(): + result[k] = map(decode_utf8, v) + + return result + + def parse_request_body(self, body): + """ Attempt to parse the post based on the content-type passed. Return the regular body if not """ + + PARSING_FUNCTIONS = { + 'application/json': json.loads, + 'text/json': json.loads, + 'application/x-www-form-urlencoded': self.parse_querystring, + } + FALLBACK_FUNCTION = lambda x: x + + content_type = self.headers.get('content-type', '') + + do_parse = PARSING_FUNCTIONS.get(content_type, FALLBACK_FUNCTION) + try: + return do_parse(body) + except: + return body + + +class EmptyRequestHeaders(dict): + pass + + +class HTTPrettyRequestEmpty(object): + body = '' + headers = EmptyRequestHeaders() + + +class FakeSockFile(StringIO): + pass + + +class FakeSSLSocket(object): + def __init__(self, sock, *args, **kw): + self._httpretty_sock = sock + + def __getattr__(self, attr): + return getattr(self._httpretty_sock, attr) + + +class fakesock(object): + class socket(object): + _entry = None + debuglevel = 0 + _sent_data = [] + + def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, + protocol=0): + self.setsockopt(family, type, protocol) + self.truesock = old_socket(family, type, protocol) + self._closed = True + self.fd = FakeSockFile() + self.timeout = socket._GLOBAL_DEFAULT_TIMEOUT + self._sock = self + self.is_http = False + self._bufsize = 16 + + def getpeercert(self, *a, **kw): + now = datetime.now() + shift = now + timedelta(days=30 * 12) + return { + 'notAfter': shift.strftime('%b %d %H:%M:%S GMT'), + 'subjectAltName': ( + ('DNS', '*%s' % self._host), + ('DNS', self._host), + ('DNS', '*'), + ), + 'subject': ( + ( + ('organizationName', '*.%s' % self._host), + ), + ( + ('organizationalUnitName', + 'Domain Control Validated'), + ), + ( + ('commonName', '*.%s' % self._host), + ), + ), + } + + def ssl(self, sock, *args, **kw): + return sock + + def setsockopt(self, family, type, protocol): + self.family = family + self.protocol = protocol + self.type = type + + def connect(self, address): + self._address = (self._host, self._port) = address + self._closed = False + self.is_http = self._port in POTENTIAL_HTTP_PORTS + + if not self.is_http: + self.truesock.connect(self._address) + + def close(self): + if not (self.is_http and self._closed): + self.truesock.close() + self._closed = True + + def makefile(self, mode='r', bufsize=-1): + """Returns this fake socket's own StringIO buffer. + + If there is an entry associated with the socket, the file + descriptor gets filled in with the entry data before being + returned. + """ + self._mode = mode + self._bufsize = bufsize + + if self._entry: + self._entry.fill_filekind(self.fd) + + return self.fd + + def real_sendall(self, data, *args, **kw): + """Sends data to the remote server. This method is called + when HTTPretty identifies that someone is trying to send + non-http data. + + The received bytes are written in this socket's StringIO + buffer so that HTTPretty can return it accordingly when + necessary. + """ + if self.is_http: # no need to connect if `self.is_http` is + # False because self.connect already did + # that + self.truesock.connect(self._address) + + self.truesock.settimeout(0) + self.truesock.sendall(data, *args, **kw) + + should_continue = True + while should_continue: + try: + received = self.truesock.recv(self._bufsize) + self.fd.write(received) + should_continue = len(received) > 0 + + except socket.error as e: + if e.errno == EAGAIN: + continue + break + + self.fd.seek(0) + + def sendall(self, data, *args, **kw): + self._sent_data.append(data) + + try: + requestline, _ = data.split(b'\r\n', 1) + method, path, version = parse_requestline(requestline) + is_parsing_headers = True + except ValueError: + is_parsing_headers = False + + if not self._entry: + # If the previous request wasn't mocked, don't mock the subsequent sending of data + return self.real_sendall(data, *args, **kw) + + self.fd.seek(0) + + if not is_parsing_headers: + if len(self._sent_data) > 1: + headers = utf8(last_requestline(self._sent_data)) + meta = dict(self._entry.request.headers) + body = utf8(self._sent_data[-1]) + if meta.get('transfer-encoding', '') == 'chunked': + if not body.isdigit() and body != '\r\n' and body != '0\r\n\r\n': + self._entry.request.body += body + else: + self._entry.request.body += body + + httpretty.historify_request(headers, body, False) + return + + # path might come with + s = urlsplit(path) + POTENTIAL_HTTP_PORTS.add(int(s.port or 80)) + headers, body = map(utf8, data.split(b'\r\n\r\n', 1)) + + request = httpretty.historify_request(headers, body) + + info = URIInfo(hostname=self._host, port=self._port, + path=s.path, + query=s.query, + last_request=request) + + matcher, entries = httpretty.match_uriinfo(info) + + if not entries: + self._entry = None + self.real_sendall(data) + return + + self._entry = matcher.get_next_entry(method, info, request) + + def debug(self, func, *a, **kw): + if self.is_http: + frame = inspect.stack()[0][0] + lines = map(utf8, traceback.format_stack(frame)) + + message = [ + "HTTPretty intercepted and unexpected socket method call.", + ("Please open an issue at " + "'https://github.com/gabrielfalcao/HTTPretty/issues'"), + "And paste the following traceback:\n", + "".join(decode_utf8(lines)), + ] + raise RuntimeError("\n".join(message)) + return func(*a, **kw) + + def settimeout(self, new_timeout): + self.timeout = new_timeout + + def send(self, *args, **kwargs): + return self.debug(self.truesock.send, *args, **kwargs) + + def sendto(self, *args, **kwargs): + return self.debug(self.truesock.sendto, *args, **kwargs) + + def recvfrom_into(self, *args, **kwargs): + return self.debug(self.truesock.recvfrom_into, *args, **kwargs) + + def recv_into(self, *args, **kwargs): + return self.debug(self.truesock.recv_into, *args, **kwargs) + + def recvfrom(self, *args, **kwargs): + return self.debug(self.truesock.recvfrom, *args, **kwargs) + + def recv(self, *args, **kwargs): + return self.debug(self.truesock.recv, *args, **kwargs) + + def __getattr__(self, name): + return getattr(self.truesock, name) + + +def fake_wrap_socket(s, *args, **kw): + return s + + +def create_fake_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None): + s = fakesock.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) + if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: + s.settimeout(timeout) + if source_address: + s.bind(source_address) + s.connect(address) + return s + + +def fake_gethostbyname(host): + return '127.0.0.1' + + +def fake_gethostname(): + return 'localhost' + + +def fake_getaddrinfo( + host, port, family=None, socktype=None, proto=None, flags=None): + return [(2, 1, 6, '', (host, port))] + + +class Entry(BaseClass): + def __init__(self, method, uri, body, + adding_headers=None, + forcing_headers=None, + status=200, + streaming=False, + **headers): + + self.method = method + self.uri = uri + self.info = None + self.request = None + + self.body_is_callable = False + if hasattr(body, "__call__"): + self.callable_body = body + self.body = None + self.body_is_callable = True + elif isinstance(body, text_type): + self.body = utf8(body) + else: + self.body = body + + self.streaming = streaming + if not streaming and not self.body_is_callable: + self.body_length = len(self.body or '') + else: + self.body_length = 0 + + self.adding_headers = adding_headers or {} + self.forcing_headers = forcing_headers or {} + self.status = int(status) + + for k, v in headers.items(): + name = "-".join(k.split("_")).title() + self.adding_headers[name] = v + + self.validate() + + def validate(self): + content_length_keys = 'Content-Length', 'content-length' + for key in content_length_keys: + got = self.adding_headers.get( + key, self.forcing_headers.get(key, None)) + + if got is None: + continue + + try: + igot = int(got) + except ValueError: + warnings.warn( + 'HTTPretty got to register the Content-Length header ' \ + 'with "%r" which is not a number' % got, + ) + + if igot > self.body_length: + raise HTTPrettyError( + 'HTTPretty got inconsistent parameters. The header ' \ + 'Content-Length you registered expects size "%d" but ' \ + 'the body you registered for that has actually length ' \ + '"%d".' % ( + igot, self.body_length, + ) + ) + + def __str__(self): + return r'<Entry %s %s getting %d>' % ( + self.method, self.uri, self.status) + + def normalize_headers(self, headers): + new = {} + for k in headers: + new_k = '-'.join([s.lower() for s in k.split('-')]) + new[new_k] = headers[k] + + return new + + def fill_filekind(self, fk): + now = datetime.utcnow() + + headers = { + 'status': self.status, + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + 'server': 'Python/HTTPretty', + 'connection': 'close', + } + + if self.forcing_headers: + headers = self.forcing_headers + + if self.adding_headers: + headers.update(self.normalize_headers(self.adding_headers)) + + headers = self.normalize_headers(headers) + status = headers.get('status', self.status) + if self.body_is_callable: + status, headers, self.body = self.callable_body(self.request, self.info.full_url(), headers) + headers.update({ + 'content-length': len(self.body) + }) + + string_list = [ + 'HTTP/1.1 %d %s' % (status, STATUSES[status]), + ] + + if 'date' in headers: + string_list.append('date: %s' % headers.pop('date')) + + if not self.forcing_headers: + content_type = headers.pop('content-type', + 'text/plain; charset=utf-8') + + content_length = headers.pop('content-length', self.body_length) + + string_list.append('content-type: %s' % content_type) + if not self.streaming: + string_list.append('content-length: %s' % content_length) + + string_list.append('server: %s' % headers.pop('server')) + + for k, v in headers.items(): + string_list.append( + '{0}: {1}'.format(k, v), + ) + + for item in string_list: + fk.write(utf8(item) + b'\n') + + fk.write(b'\r\n') + + if self.streaming: + self.body, body = itertools.tee(self.body) + for chunk in body: + fk.write(utf8(chunk)) + else: + fk.write(utf8(self.body)) + + fk.seek(0) + + +def url_fix(s, charset='utf-8'): + scheme, netloc, path, querystring, fragment = urlsplit(s) + path = quote(path, b'/%') + querystring = quote_plus(querystring, b':&=') + return urlunsplit((scheme, netloc, path, querystring, fragment)) + + +class URIInfo(BaseClass): + def __init__(self, + username='', + password='', + hostname='', + port=80, + path='/', + query='', + fragment='', + scheme='', + last_request=None): + + self.username = username or '' + self.password = password or '' + self.hostname = hostname or '' + + if port: + port = int(port) + + elif scheme == 'https': + port = 443 + + self.port = port or 80 + self.path = path or '' + self.query = query or '' + self.scheme = scheme or (self.port == 443 and "https" or "http") + self.fragment = fragment or '' + self.last_request = last_request + + def __str__(self): + attrs = ( + 'username', + 'password', + 'hostname', + 'port', + 'path', + ) + fmt = ", ".join(['%s="%s"' % (k, getattr(self, k, '')) for k in attrs]) + return r'<httpretty.URIInfo(%s)>' % fmt + + def __hash__(self): + return hash(text_type(self)) + + def __eq__(self, other): + self_tuple = ( + self.port, + decode_utf8(self.hostname.lower()), + url_fix(decode_utf8(self.path)), + ) + other_tuple = ( + other.port, + decode_utf8(other.hostname.lower()), + url_fix(decode_utf8(other.path)), + ) + return self_tuple == other_tuple + + def full_url(self, use_querystring=True): + credentials = "" + if self.password: + credentials = "{0}:{1}@".format( + self.username, self.password) + + query = "" + if use_querystring and self.query: + query = "?{0}".format(decode_utf8(self.query)) + + result = "{scheme}://{credentials}{domain}{path}{query}".format( + scheme=self.scheme, + credentials=credentials, + domain=self.get_full_domain(), + path=decode_utf8(self.path), + query=query + ) + return result + + def get_full_domain(self): + hostname = decode_utf8(self.hostname) + if self.port not in DEFAULT_HTTP_PORTS: + return ":".join([hostname, str(self.port)]) + + return hostname + + @classmethod + def from_uri(cls, uri, entry): + result = urlsplit(uri) + POTENTIAL_HTTP_PORTS.add(int(result.port or 80)) + return cls(result.username, + result.password, + result.hostname, + result.port, + result.path, + result.query, + result.fragment, + result.scheme, + entry) + + +class URIMatcher(object): + regex = None + info = None + + def __init__(self, uri, entries, match_querystring=False): + self._match_querystring = match_querystring + if type(uri).__name__ == 'SRE_Pattern': + self.regex = uri + else: + self.info = URIInfo.from_uri(uri, entries) + + self.entries = entries + + #hash of current_entry pointers, per method. + self.current_entries = {} + + def matches(self, info): + if self.info: + return self.info == info + else: + return self.regex.search(info.full_url( + use_querystring=self._match_querystring)) + + def __str__(self): + wrap = 'URLMatcher({0})' + if self.info: + return wrap.format(text_type(self.info)) + else: + return wrap.format(self.regex.pattern) + + def get_next_entry(self, method, info, request): + """Cycle through available responses, but only once. + Any subsequent requests will receive the last response""" + + if method not in self.current_entries: + self.current_entries[method] = 0 + + #restrict selection to entries that match the requested method + entries_for_method = [e for e in self.entries if e.method == method] + + if self.current_entries[method] >= len(entries_for_method): + self.current_entries[method] = -1 + + if not self.entries or not entries_for_method: + raise ValueError('I have no entries for method %s: %s' + % (method, self)) + + entry = entries_for_method[self.current_entries[method]] + if self.current_entries[method] != -1: + self.current_entries[method] += 1 + + # Attach more info to the entry + # So the callback can be more clever about what to do + # This does also fix the case where the callback + # would be handed a compiled regex as uri instead of the + # real uri + entry.info = info + entry.request = request + return entry + + def __hash__(self): + return hash(text_type(self)) + + def __eq__(self, other): + return text_type(self) == text_type(other) + + +class httpretty(HttpBaseClass): + """The URI registration class""" + _entries = {} + latest_requests = [] + + last_request = HTTPrettyRequestEmpty() + _is_enabled = False + + @classmethod + def match_uriinfo(cls, info): + for matcher, value in cls._entries.items(): + if matcher.matches(info): + return (matcher, info) + + return (None, []) + + @classmethod + @contextlib.contextmanager + def record(cls, filename, indentation=4, encoding='utf-8'): + try: + import urllib3 + except ImportError: + raise RuntimeError('HTTPretty requires urllib3 installed for recording actual requests.') + + + http = urllib3.PoolManager() + + cls.enable() + calls = [] + def record_request(request, uri, headers): + cls.disable() + + response = http.request(request.method, uri) + calls.append({ + 'request': { + 'uri': uri, + 'method': request.method, + 'headers': dict(request.headers), + 'body': request.body, + 'querystring': request.querystring + }, + 'response': { + 'status': response.status, + 'body': response.data, + 'headers': dict(response.headers) + } + }) + cls.enable() + return response.status, response.headers, response.data + + for method in cls.METHODS: + cls.register_uri(method, re.compile(r'.*', re.M), body=record_request) + + yield + cls.disable() + with codecs.open(filename, 'w', encoding) as f: + f.write(json.dumps(calls, indent=indentation)) + + @classmethod + @contextlib.contextmanager + def playback(cls, origin): + cls.enable() + + data = json.loads(open(origin).read()) + for item in data: + uri = item['request']['uri'] + method = item['request']['method'] + cls.register_uri(method, uri, body=item['response']['body'], forcing_headers=item['response']['headers']) + + yield + cls.disable() + + @classmethod + def reset(cls): + cls._entries.clear() + cls.latest_requests = [] + cls.last_request = HTTPrettyRequestEmpty() + + @classmethod + def historify_request(cls, headers, body='', append=True): + request = HTTPrettyRequest(headers, body) + cls.last_request = request + if append or not cls.latest_requests: + cls.latest_requests.append(request) + else: + cls.latest_requests[-1] = request + return request + + @classmethod + def register_uri(cls, method, uri, body='HTTPretty :)', + adding_headers=None, + forcing_headers=None, + status=200, + responses=None, match_querystring=False, + **headers): + + uri_is_string = isinstance(uri, basestring) + + if uri_is_string and re.search(r'^\w+://[^/]+[.]\w{2,}$', uri): + uri += '/' + + if isinstance(responses, list) and len(responses) > 0: + for response in responses: + response.uri = uri + response.method = method + entries_for_this_uri = responses + else: + headers[str('body')] = body + headers[str('adding_headers')] = adding_headers + headers[str('forcing_headers')] = forcing_headers + headers[str('status')] = status + + entries_for_this_uri = [ + cls.Response(method=method, uri=uri, **headers), + ] + + matcher = URIMatcher(uri, entries_for_this_uri, + match_querystring) + if matcher in cls._entries: + matcher.entries.extend(cls._entries[matcher]) + del cls._entries[matcher] + + cls._entries[matcher] = entries_for_this_uri + + def __str__(self): + return '<HTTPretty with %d URI entries>' % len(self._entries) + + @classmethod + def Response(cls, body, method=None, uri=None, adding_headers=None, forcing_headers=None, + status=200, streaming=False, **headers): + + headers[str('body')] = body + headers[str('adding_headers')] = adding_headers + headers[str('forcing_headers')] = forcing_headers + headers[str('status')] = int(status) + headers[str('streaming')] = streaming + return Entry(method, uri, **headers) + + @classmethod + def disable(cls): + cls._is_enabled = False + socket.socket = old_socket + socket.SocketType = old_socket + socket._socketobject = old_socket + + socket.create_connection = old_create_connection + socket.gethostname = old_gethostname + socket.gethostbyname = old_gethostbyname + socket.getaddrinfo = old_getaddrinfo + + socket.__dict__['socket'] = old_socket + socket.__dict__['_socketobject'] = old_socket + socket.__dict__['SocketType'] = old_socket + + socket.__dict__['create_connection'] = old_create_connection + socket.__dict__['gethostname'] = old_gethostname + socket.__dict__['gethostbyname'] = old_gethostbyname + socket.__dict__['getaddrinfo'] = old_getaddrinfo + + if socks: + socks.socksocket = old_socksocket + socks.__dict__['socksocket'] = old_socksocket + + if ssl: + ssl.wrap_socket = old_ssl_wrap_socket + ssl.SSLSocket = old_sslsocket + ssl.__dict__['wrap_socket'] = old_ssl_wrap_socket + ssl.__dict__['SSLSocket'] = old_sslsocket + + if not PY3: + ssl.sslwrap_simple = old_sslwrap_simple + ssl.__dict__['sslwrap_simple'] = old_sslwrap_simple + + @classmethod + def is_enabled(cls): + return cls._is_enabled + + @classmethod + def enable(cls): + cls._is_enabled = True + socket.socket = fakesock.socket + socket._socketobject = fakesock.socket + socket.SocketType = fakesock.socket + + socket.create_connection = create_fake_connection + socket.gethostname = fake_gethostname + socket.gethostbyname = fake_gethostbyname + socket.getaddrinfo = fake_getaddrinfo + + socket.__dict__['socket'] = fakesock.socket + socket.__dict__['_socketobject'] = fakesock.socket + socket.__dict__['SocketType'] = fakesock.socket + + socket.__dict__['create_connection'] = create_fake_connection + socket.__dict__['gethostname'] = fake_gethostname + socket.__dict__['gethostbyname'] = fake_gethostbyname + socket.__dict__['getaddrinfo'] = fake_getaddrinfo + + if socks: + socks.socksocket = fakesock.socket + socks.__dict__['socksocket'] = fakesock.socket + + if ssl: + ssl.wrap_socket = fake_wrap_socket + ssl.SSLSocket = FakeSSLSocket + + ssl.__dict__['wrap_socket'] = fake_wrap_socket + ssl.__dict__['SSLSocket'] = FakeSSLSocket + + if not PY3: + ssl.sslwrap_simple = fake_wrap_socket + ssl.__dict__['sslwrap_simple'] = fake_wrap_socket + + +def httprettified(test): + "A decorator tests that use HTTPretty" + def decorate_class(klass): + for attr in dir(klass): + if not attr.startswith('test_'): + continue + + attr_value = getattr(klass, attr) + if not hasattr(attr_value, "__call__"): + continue + + setattr(klass, attr, decorate_callable(attr_value)) + return klass + + def decorate_callable(test): + @functools.wraps(test) + def wrapper(*args, **kw): + httpretty.reset() + httpretty.enable() + try: + return test(*args, **kw) + finally: + httpretty.disable() + return wrapper + + if isinstance(test, ClassTypes): + return decorate_class(test) + return decorate_callable(test) diff --git a/httpretty/errors.py b/httpretty/errors.py new file mode 100644 index 0000000..946ba9b --- /dev/null +++ b/httpretty/errors.py @@ -0,0 +1,31 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + + +class HTTPrettyError(Exception): + pass diff --git a/httpretty/http.py b/httpretty/http.py new file mode 100644 index 0000000..388a73a --- /dev/null +++ b/httpretty/http.py @@ -0,0 +1,154 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +import re +from .compat import BaseClass + + +STATUSES = { + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 306: "Switch Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request a Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", + 420: "Enhance Your Calm", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 424: "Method Failure", + 425: "Unordered Collection", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 444: "No Response", + 449: "Retry With", + 450: "Blocked by Windows Parental Controls", + 451: "Unavailable For Legal Reasons", + 451: "Redirect", + 494: "Request Header Too Large", + 495: "Cert Error", + 496: "No Cert", + 497: "HTTP to HTTPS", + 499: "Client Closed Request", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 509: "Bandwidth Limit Exceeded", + 510: "Not Extended", + 511: "Network Authentication Required", + 598: "Network read timeout error", + 599: "Network connect timeout error", +} + + +class HttpBaseClass(BaseClass): + GET = b'GET' + PUT = b'PUT' + POST = b'POST' + DELETE = b'DELETE' + HEAD = b'HEAD' + PATCH = b'PATCH' + OPTIONS = b'OPTIONS' + CONNECT = b'CONNECT' + METHODS = (GET, PUT, POST, DELETE, HEAD, PATCH, OPTIONS, CONNECT) + + +def parse_requestline(s): + """ + http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5 + + >>> parse_requestline('GET / HTTP/1.0') + ('GET', '/', '1.0') + >>> parse_requestline('post /testurl htTP/1.1') + ('POST', '/testurl', '1.1') + >>> parse_requestline('Im not a RequestLine') + Traceback (most recent call last): + ... + ValueError: Not a Request-Line + """ + methods = b'|'.join(HttpBaseClass.METHODS) + m = re.match(br'(' + methods + b')\s+(.*)\s+HTTP/(1.[0|1])', s, re.I) + if m: + return m.group(1).upper(), m.group(2), m.group(3) + else: + raise ValueError('Not a Request-Line') + + +def last_requestline(sent_data): + """ + Find the last line in sent_data that can be parsed with parse_requestline + """ + for line in reversed(sent_data): + try: + parse_requestline(line) + except ValueError: + pass + else: + return line diff --git a/httpretty/utils.py b/httpretty/utils.py new file mode 100644 index 0000000..0480f43 --- /dev/null +++ b/httpretty/utils.py @@ -0,0 +1,44 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +from .compat import ( + byte_type, text_type +) + + +def utf8(s): + if isinstance(s, text_type): + s = s.encode('utf-8') + + return byte_type(s) + + +def decode_utf8(s): + if isinstance(s, byte_type): + s = s.decode("utf-8") + + return text_type(s) diff --git a/index.html b/index.html new file mode 100644 index 0000000..6cb8f39 --- /dev/null +++ b/index.html @@ -0,0 +1,388 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>HTTPretty by gabrielfalcao</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <!-- Loading Bootstrap --> + + <link rel='stylesheet prefetch' href='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.css'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Montserrat:400,700'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700,900,200italic,300italic,400italic,600italic,700italic,900italic'> + <link rel='stylesheet prefetch' href='//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css'> + <link href="./assets/css/github.css" rel="stylesheet"> + <link href="./assets/css/style.css" rel="stylesheet"> + <script src="./assets/js/prefixfree.min.js"></script> + </head> + <body> + + + <div class="uk-container uk-container-center" style="padding: 120px 0;"> + <div class="uk-grid" data-uk-grid-margin=""> + <div class="uk-width-medium-1-4 uk-hidden-small"> + <div style="clear:both; padding-bottom: 50px;"> + <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/httpretty-logo_1.svg" width="240" title="HTTPretty" alt="HTTPretty" /> + <h2>HTTPretty v0.7.0</h2> + <p> + <iframe src="http://instanc.es/bin/btn/watchers-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/forks-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/follow-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> +</p> + <a class="uk-navbar-brand" href="#">Table of Contents</a> + </div> + + <ul style="clear:both;" class="uk-nav"> + + + + + + + + + <li class="uk-nav-header">What is HTTPretty ?</li> + + + + + + <li><a href="intro.html#a-more-technical-description">A more technical description</a></li> + + + + + <li><a href="intro.html#installing">Installing</a></li> + + + + + + + + + + <li><a href="intro.html#expecting-a-simple-response-body">expecting a simple response body</a></li> + + + + + + + + + + <li><a href="intro.html#the-idea-behind-httpretty--how-it-works-">The idea behind HTTPretty (how it works)</a></li> + + + + + + + + + + + + + + + + <li class="uk-nav-header">Reference</li> + + + + + + <li><a href="docs.html#testing-query-strings">testing query strings</a></li> + + + + + <li><a href="docs.html#using-the-decorator">Using the decorator</a></li> + + + + + <li><a href="docs.html#providing-status-code">Providing status code</a></li> + + + + + <li><a href="docs.html#providing-custom-heades">Providing custom heades</a></li> + + + + + + + + + + <li><a href="docs.html#rotating-responses">rotating responses</a></li> + + + + + <li><a href="docs.html#streaming-responses">streaming responses</a></li> + + + + + <li><a href="docs.html#dynamic-responses-through-callbacks">dynamic responses through callbacks</a></li> + + + + + <li><a href="docs.html#matching-regular-expressions">matching regular expressions</a></li> + + + + + <li><a href="docs.html#expect-for-a-response--and-check-the-request-got-by-the--quot-server-quot--to-make-sure-it-was-fine-">expect for a response, and check the request got by the "server" to make sure it was fine.</a></li> + + + + + <li><a href="docs.html#checking-if-is-enabled">checking if is enabled</a></li> + + + + + + + + + + + + + + <li class="uk-nav-header">Acknowledgements</li> + + + + + + <li><a href="about.html#caveats">caveats</a></li> + + + + + + + + + + <li><a href="about.html#supported-libraries">supported libraries</a></li> + + + + + + + + + + + + + + + + + + + + <li class="uk-nav-header">Hacking on HTTPretty</li> + + + + + + <li><a href="contributing.html#creating-a-virtual-env">creating a virtual env</a></li> + + + + + <li><a href="contributing.html#installing-the-dependencies">installing the dependencies</a></li> + + + + + <li><a href="contributing.html#next-steps">next steps</a></li> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <li class="uk-nav-header">Release Notes</li> + + + + + + <li><a href="NEWS.html#0-7-0--current-">0.7.0 (current)</a></li> + + + + + <li><a href="NEWS.html#0-6-5">0.6.5</a></li> + + + + + <li><a href="NEWS.html#0-6-2">0.6.2</a></li> + + + + + <li><a href="NEWS.html#0-6-1">0.6.1</a></li> + + + + + <li><a href="NEWS.html#0-5-14">0.5.14</a></li> + + + + + <li><a href="NEWS.html#0-5-12">0.5.12</a></li> + + + + + <li><a href="NEWS.html#0-5-11">0.5.11</a></li> + + + + + <li><a href="NEWS.html#0-5-10">0.5.10</a></li> + + + + + <li><a href="NEWS.html#0-5-9">0.5.9</a></li> + + + + + <li><a href="NEWS.html#0-5-8">0.5.8</a></li> + + + + + + + + </ul> + </div> + <div class="uk-width-medium-3-4"> + <article class="uk-article"> + <h1 class="uk-article-title">HTTPretty</h1> + <p class="uk-article-lead">HTTP request mock tool for python</p> + <div class="uk-width-medium-1-1"> + <h1 id="what-is-httpretty--" name="what-is-httpretty--"><a href="#what-is-httpretty--">What is HTTPretty ?</a></h1> +<p>Once upon a time a python developer wanted to use a RESTful api, +everything was fine but until the day he needed to test the code that +hits the RESTful API: what if the API server is down? What if its +content has changed ?</p> + +<p>Don't worry, HTTPretty is here for you:</p> +<div class="highlight"><pre name="what-is-httpretty---example-1"><span class="kn">import</span> <span class="nn">requests</span> +<span class="kn">from</span> <span class="nn">sure</span> <span class="kn">import</span> <span class="n">expect</span> +<span class="kn">import</span> <span class="nn">httpretty</span> + + +<span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_yipit_api_returning_deals</span><span class="p">():</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="s">"http://api.yipit.com/v1/deals/"</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="s">'[{"title": "Test Deal"}]'</span><span class="p">,</span> + <span class="n">content_type</span><span class="o">=</span><span class="s">"application/json"</span><span class="p">)</span> + + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://api.yipit.com/v1/deals/'</span><span class="p">)</span> + + <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">())</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">([{</span><span class="s">"title"</span><span class="p">:</span> <span class="s">"Test Deal"</span><span class="p">}])</span> +</pre></div><h2 id="a-more-technical-description" name="a-more-technical-description"><a href="#a-more-technical-description">A more technical description</a></h2> +<p>HTTPretty is a HTTP client mock library for Python 100% inspired on ruby's <a href="http://fakeweb.rubyforge.org/">FakeWeb</a>. +If you come from ruby this would probably sound familiar :smiley:</p> +<h2 id="installing" name="installing"><a href="#installing">Installing</a></h2> +<p>Installing httpretty is as easy as:</p> +<div class="highlight"><pre name="what-is-httpretty---example-2">pip install HTTPretty +</pre></div><h1 id="demo" name="demo"><a href="#demo">Demo</a></h1><h2 id="expecting-a-simple-response-body" name="expecting-a-simple-response-body"><a href="#expecting-a-simple-response-body">expecting a simple response body</a></h2><div class="highlight"><pre name="demo-example-1"><span class="kn">import</span> <span class="nn">requests</span> +<span class="kn">import</span> <span class="nn">httpretty</span> + +<span class="k">def</span> <span class="nf">test_one</span><span class="p">():</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">enable</span><span class="p">()</span> <span class="c"># enable HTTPretty so that it will monkey patch the socket module</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="s">"http://yipit.com/"</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="s">"Find the best daily deals"</span><span class="p">)</span> + + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://yipit.com'</span><span class="p">)</span> + + <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">text</span> <span class="o">==</span> <span class="s">"Find the best daily deals"</span> + + <span class="n">httpretty</span><span class="o">.</span><span class="n">disable</span><span class="p">()</span> <span class="c"># disable afterwards, so that you will have no problems in code that uses that socket module</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">reset</span><span class="p">()</span> <span class="c"># reset HTTPretty state (clean up registered urls and request history)</span> +</pre></div><h1 id="motivation" name="motivation"><a href="#motivation">Motivation</a></h1> +<p>When building systems that access external resources such as RESTful +webservices, XMLRPC or even simple HTTP requests, we stumble in the +problem:</p> +<div class="highlight"><pre name="motivation-example-1"><span class="s">"I'm gonna need to mock all those requests"</span> +</pre></div> +<p>It brings a lot of hassle, you will need to use a generic mocking +tool, mess with scope and so on.</p> +<h2 id="the-idea-behind-httpretty--how-it-works-" name="the-idea-behind-httpretty--how-it-works-"><a href="#the-idea-behind-httpretty--how-it-works-">The idea behind HTTPretty (how it works)</a></h2> +<p>HTTPretty <a href="http://en.wikipedia.org/wiki/Monkey_patch">monkey patches</a> +Python's <a href="http://docs.python.org/library/socket.html">socket</a> core +module, reimplementing the HTTP protocol, by mocking requests and +responses.</p> + +<p>As for it works in this way, you don't need to worry what http library +you're gonna use.</p> + +<p>HTTPretty will mock the response for you :) <em>(and also give you the latest requests so that you can check them)</em></p> + + </div> + </div> + </article> + </div> + </div> + </div> + <script type="text/javascript"> + var _gaq = _gaq || []; + _gaq.push(['_setAccount', 'UA-1277640-14']); + _gaq.push(['_trackPageview']); + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); + </script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/jquery-2.0.3.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/angular.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.js'></script> + </body> +</html>
\ No newline at end of file diff --git a/intro.html b/intro.html new file mode 100644 index 0000000..6cb8f39 --- /dev/null +++ b/intro.html @@ -0,0 +1,388 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>HTTPretty by gabrielfalcao</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <!-- Loading Bootstrap --> + + <link rel='stylesheet prefetch' href='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.css'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Montserrat:400,700'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700,900,200italic,300italic,400italic,600italic,700italic,900italic'> + <link rel='stylesheet prefetch' href='//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css'> + <link href="./assets/css/github.css" rel="stylesheet"> + <link href="./assets/css/style.css" rel="stylesheet"> + <script src="./assets/js/prefixfree.min.js"></script> + </head> + <body> + + + <div class="uk-container uk-container-center" style="padding: 120px 0;"> + <div class="uk-grid" data-uk-grid-margin=""> + <div class="uk-width-medium-1-4 uk-hidden-small"> + <div style="clear:both; padding-bottom: 50px;"> + <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/httpretty-logo_1.svg" width="240" title="HTTPretty" alt="HTTPretty" /> + <h2>HTTPretty v0.7.0</h2> + <p> + <iframe src="http://instanc.es/bin/btn/watchers-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/forks-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/follow-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> +</p> + <a class="uk-navbar-brand" href="#">Table of Contents</a> + </div> + + <ul style="clear:both;" class="uk-nav"> + + + + + + + + + <li class="uk-nav-header">What is HTTPretty ?</li> + + + + + + <li><a href="intro.html#a-more-technical-description">A more technical description</a></li> + + + + + <li><a href="intro.html#installing">Installing</a></li> + + + + + + + + + + <li><a href="intro.html#expecting-a-simple-response-body">expecting a simple response body</a></li> + + + + + + + + + + <li><a href="intro.html#the-idea-behind-httpretty--how-it-works-">The idea behind HTTPretty (how it works)</a></li> + + + + + + + + + + + + + + + + <li class="uk-nav-header">Reference</li> + + + + + + <li><a href="docs.html#testing-query-strings">testing query strings</a></li> + + + + + <li><a href="docs.html#using-the-decorator">Using the decorator</a></li> + + + + + <li><a href="docs.html#providing-status-code">Providing status code</a></li> + + + + + <li><a href="docs.html#providing-custom-heades">Providing custom heades</a></li> + + + + + + + + + + <li><a href="docs.html#rotating-responses">rotating responses</a></li> + + + + + <li><a href="docs.html#streaming-responses">streaming responses</a></li> + + + + + <li><a href="docs.html#dynamic-responses-through-callbacks">dynamic responses through callbacks</a></li> + + + + + <li><a href="docs.html#matching-regular-expressions">matching regular expressions</a></li> + + + + + <li><a href="docs.html#expect-for-a-response--and-check-the-request-got-by-the--quot-server-quot--to-make-sure-it-was-fine-">expect for a response, and check the request got by the "server" to make sure it was fine.</a></li> + + + + + <li><a href="docs.html#checking-if-is-enabled">checking if is enabled</a></li> + + + + + + + + + + + + + + <li class="uk-nav-header">Acknowledgements</li> + + + + + + <li><a href="about.html#caveats">caveats</a></li> + + + + + + + + + + <li><a href="about.html#supported-libraries">supported libraries</a></li> + + + + + + + + + + + + + + + + + + + + <li class="uk-nav-header">Hacking on HTTPretty</li> + + + + + + <li><a href="contributing.html#creating-a-virtual-env">creating a virtual env</a></li> + + + + + <li><a href="contributing.html#installing-the-dependencies">installing the dependencies</a></li> + + + + + <li><a href="contributing.html#next-steps">next steps</a></li> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <li class="uk-nav-header">Release Notes</li> + + + + + + <li><a href="NEWS.html#0-7-0--current-">0.7.0 (current)</a></li> + + + + + <li><a href="NEWS.html#0-6-5">0.6.5</a></li> + + + + + <li><a href="NEWS.html#0-6-2">0.6.2</a></li> + + + + + <li><a href="NEWS.html#0-6-1">0.6.1</a></li> + + + + + <li><a href="NEWS.html#0-5-14">0.5.14</a></li> + + + + + <li><a href="NEWS.html#0-5-12">0.5.12</a></li> + + + + + <li><a href="NEWS.html#0-5-11">0.5.11</a></li> + + + + + <li><a href="NEWS.html#0-5-10">0.5.10</a></li> + + + + + <li><a href="NEWS.html#0-5-9">0.5.9</a></li> + + + + + <li><a href="NEWS.html#0-5-8">0.5.8</a></li> + + + + + + + + </ul> + </div> + <div class="uk-width-medium-3-4"> + <article class="uk-article"> + <h1 class="uk-article-title">HTTPretty</h1> + <p class="uk-article-lead">HTTP request mock tool for python</p> + <div class="uk-width-medium-1-1"> + <h1 id="what-is-httpretty--" name="what-is-httpretty--"><a href="#what-is-httpretty--">What is HTTPretty ?</a></h1> +<p>Once upon a time a python developer wanted to use a RESTful api, +everything was fine but until the day he needed to test the code that +hits the RESTful API: what if the API server is down? What if its +content has changed ?</p> + +<p>Don't worry, HTTPretty is here for you:</p> +<div class="highlight"><pre name="what-is-httpretty---example-1"><span class="kn">import</span> <span class="nn">requests</span> +<span class="kn">from</span> <span class="nn">sure</span> <span class="kn">import</span> <span class="n">expect</span> +<span class="kn">import</span> <span class="nn">httpretty</span> + + +<span class="nd">@httpretty.activate</span> +<span class="k">def</span> <span class="nf">test_yipit_api_returning_deals</span><span class="p">():</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="s">"http://api.yipit.com/v1/deals/"</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="s">'[{"title": "Test Deal"}]'</span><span class="p">,</span> + <span class="n">content_type</span><span class="o">=</span><span class="s">"application/json"</span><span class="p">)</span> + + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://api.yipit.com/v1/deals/'</span><span class="p">)</span> + + <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">())</span><span class="o">.</span><span class="n">to</span><span class="o">.</span><span class="n">equal</span><span class="p">([{</span><span class="s">"title"</span><span class="p">:</span> <span class="s">"Test Deal"</span><span class="p">}])</span> +</pre></div><h2 id="a-more-technical-description" name="a-more-technical-description"><a href="#a-more-technical-description">A more technical description</a></h2> +<p>HTTPretty is a HTTP client mock library for Python 100% inspired on ruby's <a href="http://fakeweb.rubyforge.org/">FakeWeb</a>. +If you come from ruby this would probably sound familiar :smiley:</p> +<h2 id="installing" name="installing"><a href="#installing">Installing</a></h2> +<p>Installing httpretty is as easy as:</p> +<div class="highlight"><pre name="what-is-httpretty---example-2">pip install HTTPretty +</pre></div><h1 id="demo" name="demo"><a href="#demo">Demo</a></h1><h2 id="expecting-a-simple-response-body" name="expecting-a-simple-response-body"><a href="#expecting-a-simple-response-body">expecting a simple response body</a></h2><div class="highlight"><pre name="demo-example-1"><span class="kn">import</span> <span class="nn">requests</span> +<span class="kn">import</span> <span class="nn">httpretty</span> + +<span class="k">def</span> <span class="nf">test_one</span><span class="p">():</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">enable</span><span class="p">()</span> <span class="c"># enable HTTPretty so that it will monkey patch the socket module</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">register_uri</span><span class="p">(</span><span class="n">httpretty</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="s">"http://yipit.com/"</span><span class="p">,</span> + <span class="n">body</span><span class="o">=</span><span class="s">"Find the best daily deals"</span><span class="p">)</span> + + <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'http://yipit.com'</span><span class="p">)</span> + + <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">text</span> <span class="o">==</span> <span class="s">"Find the best daily deals"</span> + + <span class="n">httpretty</span><span class="o">.</span><span class="n">disable</span><span class="p">()</span> <span class="c"># disable afterwards, so that you will have no problems in code that uses that socket module</span> + <span class="n">httpretty</span><span class="o">.</span><span class="n">reset</span><span class="p">()</span> <span class="c"># reset HTTPretty state (clean up registered urls and request history)</span> +</pre></div><h1 id="motivation" name="motivation"><a href="#motivation">Motivation</a></h1> +<p>When building systems that access external resources such as RESTful +webservices, XMLRPC or even simple HTTP requests, we stumble in the +problem:</p> +<div class="highlight"><pre name="motivation-example-1"><span class="s">"I'm gonna need to mock all those requests"</span> +</pre></div> +<p>It brings a lot of hassle, you will need to use a generic mocking +tool, mess with scope and so on.</p> +<h2 id="the-idea-behind-httpretty--how-it-works-" name="the-idea-behind-httpretty--how-it-works-"><a href="#the-idea-behind-httpretty--how-it-works-">The idea behind HTTPretty (how it works)</a></h2> +<p>HTTPretty <a href="http://en.wikipedia.org/wiki/Monkey_patch">monkey patches</a> +Python's <a href="http://docs.python.org/library/socket.html">socket</a> core +module, reimplementing the HTTP protocol, by mocking requests and +responses.</p> + +<p>As for it works in this way, you don't need to worry what http library +you're gonna use.</p> + +<p>HTTPretty will mock the response for you :) <em>(and also give you the latest requests so that you can check them)</em></p> + + </div> + </div> + </article> + </div> + </div> + </div> + <script type="text/javascript"> + var _gaq = _gaq || []; + _gaq.push(['_setAccount', 'UA-1277640-14']); + _gaq.push(['_trackPageview']); + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); + </script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/jquery-2.0.3.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/angular.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.js'></script> + </body> +</html>
\ No newline at end of file diff --git a/requirements.pip b/requirements.pip new file mode 100644 index 0000000..5d38ec1 --- /dev/null +++ b/requirements.pip @@ -0,0 +1,12 @@ +argparse==1.2.1 +coverage==3.5.3 +httplib2==0.7.6 +misaka==1.0.2 +mock==1.0.1 +nose==1.2.1 +requests==1.1.0 +steadymark==0.4.5 +sure>=1.2.1 +tornado==2.4 +tox==1.4.2 +urllib3
\ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8690123 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,29 @@ +bolacha==0.6.0 +couleur==0.5.0 +coverage==3.5.3 +cssselect==0.8 +distribute==0.6.30 +Flask==0.9 +httplib2==0.7.6 +ipdb==0.7 +ipython==0.13.1 +Jinja2==2.6 +lxml==3.1.2 +markment==0.2.14 +misaka==1.0.2 +mock==1.0.1 +multiprocessing==2.6.2.1 +nose==1.2.1 +py==1.4.12 +Pygments==1.6 +python-qt==0.50 +PyYAML==3.10 +redis==2.7.1 +requests==1.1.0 +speakers==0.0.3 +steadymark==0.4.5 +sure==1.2.1 +tornado==2.4 +tox==1.4.2 +virtualenv==1.8.2 +Werkzeug==0.9.1 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..8f66a4b --- /dev/null +++ b/setup.py @@ -0,0 +1,60 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +import os +from httpretty import version, HTTPretty +from setuptools import setup, find_packages + +HTTPretty.disable() + +HTTPRETTY_PATH = os.path.abspath(os.path.join(__file__, os.pardir)) + + +def test_packages(): + test_reqs = os.path.join(HTTPRETTY_PATH, 'requirements.pip') + tests_require = [ + line.strip() for line in open(test_reqs).readlines() + if not line.startswith("#") + ] + return tests_require + +setup(name='httpretty', + version=version, + description='HTTP client mock for Python', + author='Gabriel Falcao', + author_email='gabriel@nacaolivre.org', + url='http://github.com/gabrielfalcao/httpretty', + zip_safe=False, + packages=find_packages(HTTPRETTY_PATH, ('tests')), + tests_require=test_packages(), + install_requires=['urllib3'], + license='MIT', + classifiers=["Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Topic :: Software Development :: Testing"], +) diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..56b5df3 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,17 @@ +<urlset> + <url> + <loc>http://falcao.it/HTTPretty/about.html</loc> + </url> + <url> + <loc>http://falcao.it/HTTPretty/contributing.html</loc> + </url> + <url> + <loc>http://falcao.it/HTTPretty/docs.html</loc> + </url> + <url> + <loc>http://falcao.it/HTTPretty/intro.html</loc> + </url> + <url> + <loc>http://falcao.it/HTTPretty/NEWS.html</loc> + </url> +</urlset> diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..9047465 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import sure diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py new file mode 100644 index 0000000..8589d02 --- /dev/null +++ b/tests/functional/__init__.py @@ -0,0 +1,28 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +import warnings +warnings.simplefilter('ignore') diff --git a/tests/functional/base.py b/tests/functional/base.py new file mode 100644 index 0000000..4808752 --- /dev/null +++ b/tests/functional/base.py @@ -0,0 +1,98 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals +import os +import threading +import traceback +import tornado.ioloop +import tornado.web +from functools import wraps +from sure import scenario +import json +from os.path import abspath, dirname, join +from httpretty.core import POTENTIAL_HTTP_PORTS + + +LOCAL_FILE = lambda *path: join(abspath(dirname(__file__)), *path) +FIXTURE_FILE = lambda name: LOCAL_FILE('fixtures', name) + + +class JSONEchoHandler(tornado.web.RequestHandler): + def get(self, matched): + payload = dict([(x, self.get_argument(x)) for x in self.request.arguments]) + self.write(json.dumps({matched or 'index': payload}, indent=4)) + + def post(self, matched): + payload = dict(self.request.arguments) + self.write(json.dumps({matched or 'index': payload}, indent=4)) + + +class JSONEchoServer(threading.Thread): + def __init__(self, lock, port=8888, *args, **kw): + self.lock = lock + self.port = int(port) + self._stop = threading.Event() + super(JSONEchoServer, self).__init__(*args, **kw) + self.daemon = True + + def stop(self): + self._stop.set() + + def stopped(self): + return self._stop.isSet() + + def setup_application(self): + return tornado.web.Application([ + (r"/(.*)", JSONEchoHandler), + ]) + + def run(self): + application = self.setup_application() + application.listen(self.port) + self.lock.release() + tornado.ioloop.IOLoop.instance().start() + + + +def use_tornado_server(callback): + lock = threading.Lock() + lock.acquire() + + @wraps(callback) + def func(*args, **kw): + server = JSONEchoServer(lock, os.getenv('TEST_PORT', 8888)) + server.start() + try: + lock.acquire() + callback(*args, **kw) + finally: + lock.release() + server.stop() + if 8888 in POTENTIAL_HTTP_PORTS: + POTENTIAL_HTTP_PORTS.remove(8888) + return func diff --git a/tests/functional/fixtures/.placeholder b/tests/functional/fixtures/.placeholder new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/functional/fixtures/.placeholder diff --git a/tests/functional/fixtures/playback-1.json b/tests/functional/fixtures/playback-1.json new file mode 100644 index 0000000..1eef307 --- /dev/null +++ b/tests/functional/fixtures/playback-1.json @@ -0,0 +1,58 @@ +[ + { + "request": { + "body": "", + "headers": { + "host": "localhost:8888", + "accept-encoding": "gzip, deflate, compress", + "content-length": "0", + "accept": "*/*", + "user-agent": "python-requests/1.1.0 CPython/2.7.5 Darwin/12.5.0" + }, + "querystring": { + "age": [ + "25" + ], + "name": [ + "Gabriel" + ] + }, + "uri": "http://localhost:8888/foobar?name=Gabriel&age=25", + "method": "GET" + }, + "response": { + "status": 200, + "body": "{\n \"foobar\": {\n \"age\": \"25\", \n \"name\": \"Gabriel\"\n }\n}", + "headers": { + "content-length": "73", + "etag": "\"6fdccaba6542114e7d1098d22a01623dc2aa5761\"", + "content-type": "text/html; charset=UTF-8", + "server": "TornadoServer/2.4" + } + } + }, + { + "request": { + "body": "{\"test\": \"123\"}", + "headers": { + "host": "localhost:8888", + "accept-encoding": "gzip, deflate, compress", + "content-length": "15", + "accept": "*/*", + "user-agent": "python-requests/1.1.0 CPython/2.7.5 Darwin/12.5.0" + }, + "querystring": {}, + "uri": "http://localhost:8888/foobar", + "method": "POST" + }, + "response": { + "status": 200, + "body": "{\n \"foobar\": {}\n}", + "headers": { + "content-length": "20", + "content-type": "text/html; charset=UTF-8", + "server": "TornadoServer/2.4" + } + } + } +]
\ No newline at end of file diff --git a/tests/functional/test_bypass.py b/tests/functional/test_bypass.py new file mode 100644 index 0000000..6409eb5 --- /dev/null +++ b/tests/functional/test_bypass.py @@ -0,0 +1,136 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <httpretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +try: + import urllib.request as urllib2 +except ImportError: + import urllib2 + +from .testserver import TornadoServer, TCPServer, TCPClient +from sure import expect, that_with_context + +import httpretty +from httpretty import core + + +def start_http_server(context): + context.server = TornadoServer(9999) + context.server.start() + httpretty.enable() + + +def stop_http_server(context): + context.server.stop() + httpretty.enable() + + +def start_tcp_server(context): + context.server = TCPServer(8888) + context.server.start() + context.client = TCPClient(8888) + httpretty.enable() + + +def stop_tcp_server(context): + context.server.stop() + context.client.close() + httpretty.enable() + + +@httpretty.activate +@that_with_context(start_http_server, stop_http_server) +def test_httpretty_bypasses_when_disabled(context): + "httpretty should bypass all requests by disabling it" + + httpretty.register_uri( + httpretty.GET, "http://localhost:9999/go-for-bubbles/", + body="glub glub") + + httpretty.disable() + + fd = urllib2.urlopen('http://localhost:9999/go-for-bubbles/') + got1 = fd.read() + fd.close() + + expect(got1).to.equal( + b'. o O 0 O o . o O 0 O o . o O 0 O o . o O 0 O o . o O 0 O o .') + + fd = urllib2.urlopen('http://localhost:9999/come-again/') + got2 = fd.read() + fd.close() + + expect(got2).to.equal(b'<- HELLO WORLD ->') + + httpretty.enable() + + fd = urllib2.urlopen('http://localhost:9999/go-for-bubbles/') + got3 = fd.read() + fd.close() + + expect(got3).to.equal(b'glub glub') + core.POTENTIAL_HTTP_PORTS.remove(9999) + +@httpretty.activate +@that_with_context(start_http_server, stop_http_server) +def test_httpretty_bypasses_a_unregistered_request(context): + "httpretty should bypass a unregistered request by disabling it" + + httpretty.register_uri( + httpretty.GET, "http://localhost:9999/go-for-bubbles/", + body="glub glub") + + fd = urllib2.urlopen('http://localhost:9999/go-for-bubbles/') + got1 = fd.read() + fd.close() + + expect(got1).to.equal(b'glub glub') + + fd = urllib2.urlopen('http://localhost:9999/come-again/') + got2 = fd.read() + fd.close() + + expect(got2).to.equal(b'<- HELLO WORLD ->') + core.POTENTIAL_HTTP_PORTS.remove(9999) + + +@httpretty.activate +@that_with_context(start_tcp_server, stop_tcp_server) +def test_using_httpretty_with_other_tcp_protocols(context): + "httpretty should work even when testing code that also use other TCP-based protocols" + + httpretty.register_uri( + httpretty.GET, "http://falcao.it/foo/", + body="BAR") + + fd = urllib2.urlopen('http://falcao.it/foo/') + got1 = fd.read() + fd.close() + + expect(got1).to.equal(b'BAR') + + expect(context.client.send("foobar")).to.equal(b"RECEIVED: foobar") diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py new file mode 100644 index 0000000..1562be6 --- /dev/null +++ b/tests/functional/test_debug.py @@ -0,0 +1,105 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals +import socket +from sure import scenario, expect +from httpretty import httprettified + + +def create_socket(context): + context.sock = socket.socket( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + ) + context.sock.is_http = True + + +@httprettified +@scenario(create_socket) +def test_httpretty_debugs_socket_send(context): + "HTTPretty should debug socket.send" + + expect(context.sock.send).when.called.to.throw( + RuntimeError, + "HTTPretty intercepted and unexpected socket method call." + ) + + +@httprettified +@scenario(create_socket) +def test_httpretty_debugs_socket_sendto(context): + "HTTPretty should debug socket.sendto" + + expect(context.sock.sendto).when.called.to.throw( + RuntimeError, + "HTTPretty intercepted and unexpected socket method call." + ) + + +@httprettified +@scenario(create_socket) +def test_httpretty_debugs_socket_recv(context): + "HTTPretty should debug socket.recv" + + expect(context.sock.recv).when.called.to.throw( + RuntimeError, + "HTTPretty intercepted and unexpected socket method call." + ) + + +@httprettified +@scenario(create_socket) +def test_httpretty_debugs_socket_recvfrom(context): + "HTTPretty should debug socket.recvfrom" + + expect(context.sock.recvfrom).when.called.to.throw( + RuntimeError, + "HTTPretty intercepted and unexpected socket method call." + ) + + +@httprettified +@scenario(create_socket) +def test_httpretty_debugs_socket_recv_into(context): + "HTTPretty should debug socket.recv_into" + + expect(context.sock.recv_into).when.called.to.throw( + RuntimeError, + "HTTPretty intercepted and unexpected socket method call." + ) + + +@httprettified +@scenario(create_socket) +def test_httpretty_debugs_socket_recvfrom_into(context): + "HTTPretty should debug socket.recvfrom_into" + + expect(context.sock.recvfrom_into).when.called.to.throw( + RuntimeError, + "HTTPretty intercepted and unexpected socket method call." + ) diff --git a/tests/functional/test_decorator.py b/tests/functional/test_decorator.py new file mode 100644 index 0000000..65f6f2a --- /dev/null +++ b/tests/functional/test_decorator.py @@ -0,0 +1,48 @@ +# coding: utf-8 +from unittest import TestCase +from sure import expect +from httpretty import httprettified, HTTPretty + +try: + import urllib.request as urllib2 +except ImportError: + import urllib2 + + +@httprettified +def test_decor(): + HTTPretty.register_uri( + HTTPretty.GET, "http://localhost/", + body="glub glub") + + fd = urllib2.urlopen('http://localhost/') + got1 = fd.read() + fd.close() + + expect(got1).to.equal(b'glub glub') + + +@httprettified +class ClassDecorator(TestCase): + + def test_decorated(self): + HTTPretty.register_uri( + HTTPretty.GET, "http://localhost/", + body="glub glub") + + fd = urllib2.urlopen('http://localhost/') + got1 = fd.read() + fd.close() + + expect(got1).to.equal(b'glub glub') + + def test_decorated2(self): + HTTPretty.register_uri( + HTTPretty.GET, "http://localhost/", + body="buble buble") + + fd = urllib2.urlopen('http://localhost/') + got1 = fd.read() + fd.close() + + expect(got1).to.equal(b'buble buble')
\ No newline at end of file diff --git a/tests/functional/test_httplib2.py b/tests/functional/test_httplib2.py new file mode 100644 index 0000000..11baedc --- /dev/null +++ b/tests/functional/test_httplib2.py @@ -0,0 +1,306 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +import re +import httplib2 +from sure import expect, within, microseconds +from httpretty import HTTPretty, httprettified +from httpretty.core import decode_utf8 + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_mock_a_simple_get_with_httplib2_read(now): + "HTTPretty should mock a simple GET with httplib2.context.http" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + _, got = httplib2.Http().request('http://yipit.com', 'GET') + expect(got).to.equal(b'Find the best daily deals') + + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/') + + +@httprettified +@within(two=microseconds) +def test_httpretty_provides_easy_access_to_querystrings(now): + "HTTPretty should provide an easy access to the querystring" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + httplib2.Http().request('http://yipit.com?foo=bar&foo=baz&chuck=norris', 'GET') + expect(HTTPretty.last_request.querystring).to.equal({ + 'foo': ['bar', 'baz'], + 'chuck': ['norris'], + }) + + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_mock_headers_httplib2(now): + "HTTPretty should mock basic headers with httplib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", + body="this is supposed to be the response", + status=201) + + headers, _ = httplib2.Http().request('http://github.com', 'GET') + expect(headers['status']).to.equal('201') + expect(dict(headers)).to.equal({ + 'content-type': 'text/plain; charset=utf-8', + 'connection': 'close', + 'content-length': '35', + 'status': '201', + 'server': 'Python/HTTPretty', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_adding_and_overwritting_httplib2(now): + "HTTPretty should allow adding and overwritting headers with httplib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", + body="this is supposed to be the response", + adding_headers={ + 'Server': 'Apache', + 'Content-Length': '27', + 'Content-Type': 'application/json', + }) + + headers, _ = httplib2.Http().request('http://github.com/foo', 'GET') + + expect(dict(headers)).to.equal({ + 'content-type': 'application/json', + 'content-location': 'http://github.com/foo', + 'connection': 'close', + 'content-length': '27', + 'status': '200', + 'server': 'Apache', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_forcing_headers_httplib2(now): + "HTTPretty should allow forcing headers with httplib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", + body="this is supposed to be the response", + forcing_headers={ + 'Content-Type': 'application/xml', + }) + + headers, _ = httplib2.Http().request('http://github.com/foo', 'GET') + + expect(dict(headers)).to.equal({ + 'content-location': 'http://github.com/foo', # httplib2 FORCES + # content-location + # even if the + # server does not + # provide it + 'content-type': 'application/xml', + 'status': '200', # httplib2 also ALWAYS put status on headers + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_adding_and_overwritting_by_kwargs_u2(now): + "HTTPretty should allow adding and overwritting headers by keyword args " \ + "with httplib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", + body="this is supposed to be the response", + server='Apache', + content_length='27', + content_type='application/json') + + headers, _ = httplib2.Http().request('http://github.com/foo', 'GET') + + expect(dict(headers)).to.equal({ + 'content-type': 'application/json', + 'content-location': 'http://github.com/foo', # httplib2 FORCES + # content-location + # even if the + # server does not + # provide it + 'connection': 'close', + 'content-length': '27', + 'status': '200', + 'server': 'Apache', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_rotating_responses_with_httplib2(now): + "HTTPretty should support rotating responses with httplib2" + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + responses=[ + HTTPretty.Response(body="first response", status=201), + HTTPretty.Response(body='second and last response', status=202), + ]) + + headers1, body1 = httplib2.Http().request( + 'https://api.yahoo.com/test', 'GET') + + expect(headers1['status']).to.equal('201') + expect(body1).to.equal(b'first response') + + headers2, body2 = httplib2.Http().request( + 'https://api.yahoo.com/test', 'GET') + + expect(headers2['status']).to.equal('202') + expect(body2).to.equal(b'second and last response') + + headers3, body3 = httplib2.Http().request( + 'https://api.yahoo.com/test', 'GET') + + expect(headers3['status']).to.equal('202') + expect(body3).to.equal(b'second and last response') + + +@httprettified +@within(two=microseconds) +def test_can_inspect_last_request(now): + "HTTPretty.last_request is a mimetools.Message request from last match" + + HTTPretty.register_uri(HTTPretty.POST, "http://api.github.com/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + headers, body = httplib2.Http().request( + 'http://api.github.com', 'POST', + body='{"username": "gabrielfalcao"}', + headers={ + 'content-type': 'text/json', + }, + ) + + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.body).to.equal( + b'{"username": "gabrielfalcao"}', + ) + expect(HTTPretty.last_request.headers['content-type']).to.equal( + 'text/json', + ) + expect(body).to.equal(b'{"repositories": ["HTTPretty", "lettuce"]}') + + +@httprettified +@within(two=microseconds) +def test_can_inspect_last_request_with_ssl(now): + "HTTPretty.last_request is recorded even when mocking 'https' (SSL)" + + HTTPretty.register_uri(HTTPretty.POST, "https://secure.github.com/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + headers, body = httplib2.Http().request( + 'https://secure.github.com', 'POST', + body='{"username": "gabrielfalcao"}', + headers={ + 'content-type': 'text/json', + }, + ) + + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.body).to.equal( + b'{"username": "gabrielfalcao"}', + ) + expect(HTTPretty.last_request.headers['content-type']).to.equal( + 'text/json', + ) + expect(body).to.equal(b'{"repositories": ["HTTPretty", "lettuce"]}') + + +@httprettified +@within(two=microseconds) +def test_httpretty_ignores_querystrings_from_registered_uri(now): + "Registering URIs with query string cause them to be ignored" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/?id=123", + body="Find the best daily deals") + + _, got = httplib2.Http().request('http://yipit.com/?id=123', 'GET') + + expect(got).to.equal(b'Find the best daily deals') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/?id=123') + + +@httprettified +@within(two=microseconds) +def test_callback_response(now): + ("HTTPretty should all a callback function to be set as the body with" + " httplib2") + + def request_callback(request, uri, headers): + return [200,headers,"The {0} response from {1}".format(decode_utf8(request.method), uri)] + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + body=request_callback) + + headers1, body1 = httplib2.Http().request( + 'https://api.yahoo.com/test', 'GET') + + expect(body1).to.equal(b"The GET response from https://api.yahoo.com/test") + + HTTPretty.register_uri( + HTTPretty.POST, "https://api.yahoo.com/test_post", + body=request_callback) + + headers2, body2 = httplib2.Http().request( + 'https://api.yahoo.com/test_post', 'POST') + + expect(body2).to.equal(b"The POST response from https://api.yahoo.com/test_post") + + +@httprettified +def test_httpretty_should_allow_registering_regexes(): + "HTTPretty should allow registering regexes with httplib2" + + HTTPretty.register_uri( + HTTPretty.GET, + re.compile("https://api.yipit.com/v1/deal;brand=(?P<brand_name>\w+)"), + body="Found brand", + ) + + response, body = httplib2.Http().request('https://api.yipit.com/v1/deal;brand=gap', 'GET') + expect(body).to.equal(b'Found brand') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/v1/deal;brand=gap') diff --git a/tests/functional/test_requests.py b/tests/functional/test_requests.py new file mode 100644 index 0000000..0a4bf55 --- /dev/null +++ b/tests/functional/test_requests.py @@ -0,0 +1,751 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals + +import os +import re +import json +import requests +from sure import within, microseconds, expect +from httpretty import HTTPretty, httprettified +from httpretty.core import decode_utf8 + +from base import FIXTURE_FILE, use_tornado_server + +try: + xrange = xrange +except NameError: + xrange = range + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + +PORT = int(os.getenv('TEST_PORT') or 8888) +server_url = lambda path: "http://localhost:{0}/{1}".format(PORT, path.lstrip('/')) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_mock_a_simple_get_with_requests_read(now): + "HTTPretty should mock a simple GET with requests.get" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + response = requests.get('http://yipit.com') + expect(response.text).to.equal('Find the best daily deals') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/') + + +@httprettified +@within(two=microseconds) +def test_httpretty_provides_easy_access_to_querystrings(now): + "HTTPretty should provide an easy access to the querystring" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + requests.get('http://yipit.com/?foo=bar&foo=baz&chuck=norris') + expect(HTTPretty.last_request.querystring).to.equal({ + 'foo': ['bar', 'baz'], + 'chuck': ['norris'], + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_mock_headers_requests(now): + "HTTPretty should mock basic headers with requests" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", + body="this is supposed to be the response", + status=201) + + response = requests.get('http://github.com') + expect(response.status_code).to.equal(201) + + expect(dict(response.headers)).to.equal({ + 'content-type': 'text/plain; charset=utf-8', + 'connection': 'close', + 'content-length': '35', + 'status': '201', + 'server': 'Python/HTTPretty', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_adding_and_overwritting_requests(now): + "HTTPretty should allow adding and overwritting headers with requests" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", + body="this is supposed to be the response", + adding_headers={ + 'Server': 'Apache', + 'Content-Length': '27', + 'Content-Type': 'application/json', + }) + + response = requests.get('http://github.com/foo') + + expect(dict(response.headers)).to.equal({ + 'content-type': 'application/json', + 'connection': 'close', + 'content-length': '27', + 'status': '200', + 'server': 'Apache', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_forcing_headers_requests(now): + "HTTPretty should allow forcing headers with requests" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", + body="<root><baz /</root>", + forcing_headers={ + 'Content-Type': 'application/xml', + 'Content-Length': '19', + }) + + response = requests.get('http://github.com/foo') + + expect(dict(response.headers)).to.equal({ + 'content-type': 'application/xml', + 'content-length': '19', + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_adding_and_overwritting_by_kwargs_u2(now): + "HTTPretty should allow adding and overwritting headers by keyword args " \ + "with requests" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", + body="this is supposed to be the response", + server='Apache', + content_length='27', + content_type='application/json') + + response = requests.get('http://github.com/foo') + + expect(dict(response.headers)).to.equal({ + 'content-type': 'application/json', + 'connection': 'close', + 'content-length': '27', + 'status': '200', + 'server': 'Apache', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_rotating_responses_with_requests(now): + "HTTPretty should support rotating responses with requests" + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + responses=[ + HTTPretty.Response(body=b"first response", status=201), + HTTPretty.Response(body=b'second and last response', status=202), + ]) + + response1 = requests.get( + 'https://api.yahoo.com/test') + + expect(response1.status_code).to.equal(201) + expect(response1.text).to.equal('first response') + + response2 = requests.get( + 'https://api.yahoo.com/test') + + expect(response2.status_code).to.equal(202) + expect(response2.text).to.equal('second and last response') + + response3 = requests.get( + 'https://api.yahoo.com/test') + + expect(response3.status_code).to.equal(202) + expect(response3.text).to.equal('second and last response') + + +@httprettified +@within(two=microseconds) +def test_can_inspect_last_request(now): + "HTTPretty.last_request is a mimetools.Message request from last match" + + HTTPretty.register_uri(HTTPretty.POST, "http://api.github.com/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + response = requests.post( + 'http://api.github.com', + '{"username": "gabrielfalcao"}', + headers={ + 'content-type': 'text/json', + }, + ) + + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.body).to.equal( + b'{"username": "gabrielfalcao"}', + ) + expect(HTTPretty.last_request.headers['content-type']).to.equal( + 'text/json', + ) + expect(response.json()).to.equal({"repositories": ["HTTPretty", "lettuce"]}) + + +@httprettified +@within(two=microseconds) +def test_can_inspect_last_request_with_ssl(now): + "HTTPretty.last_request is recorded even when mocking 'https' (SSL)" + + HTTPretty.register_uri(HTTPretty.POST, "https://secure.github.com/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + response = requests.post( + 'https://secure.github.com', + '{"username": "gabrielfalcao"}', + headers={ + 'content-type': 'text/json', + }, + ) + + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.body).to.equal( + b'{"username": "gabrielfalcao"}', + ) + expect(HTTPretty.last_request.headers['content-type']).to.equal( + 'text/json', + ) + expect(response.json()).to.equal({"repositories": ["HTTPretty", "lettuce"]}) + + +@httprettified +@within(two=microseconds) +def test_httpretty_ignores_querystrings_from_registered_uri(now): + "HTTPretty should ignore querystrings from the registered uri (requests library)" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/?id=123", + body=b"Find the best daily deals") + + response = requests.get('http://yipit.com/', params={'id': 123}) + expect(response.text).to.equal('Find the best daily deals') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/?id=123') + + +@httprettified +@within(five=microseconds) +def test_streaming_responses(now): + """ + Mock a streaming HTTP response, like those returned by the Twitter streaming + API. + """ + from contextlib import contextmanager + + @contextmanager + def in_time(time, message): + """ + A context manager that uses signals to force a time limit in tests + (unlike the `@within` decorator, which only complains afterward), or + raise an AssertionError. + """ + import signal + + def handler(signum, frame): + raise AssertionError(message) + signal.signal(signal.SIGALRM, handler) + signal.setitimer(signal.ITIMER_REAL, time) + yield + signal.setitimer(signal.ITIMER_REAL, 0) + + #XXX this obviously isn't a fully functional twitter streaming client! + twitter_response_lines = [ + b'{"text":"If \\"for the boobs\\" requests to follow me one more time I\'m calling the police. http://t.co/a0mDEAD8"}\r\n', + b'\r\n', + b'{"text":"RT @onedirection: Thanks for all your #FollowMe1D requests Directioners! We\u2019ll be following 10 people throughout the day starting NOW. G ..."}\r\n' + ] + + TWITTER_STREAMING_URL = "https://stream.twitter.com/1/statuses/filter.json" + + HTTPretty.register_uri(HTTPretty.POST, TWITTER_STREAMING_URL, + body=(l for l in twitter_response_lines), + streaming=True) + + # taken from the requests docs + # Http://docs.python-requests.org/en/latest/user/advanced/#streaming-requests + response = requests.post(TWITTER_STREAMING_URL, data={'track': 'requests'}, + auth=('username', 'password'), stream=True) + + #test iterating by line + line_iter = response.iter_lines() + with in_time(0.01, 'Iterating by line is taking forever!'): + for i in xrange(len(twitter_response_lines)): + expect(next(line_iter).strip()).to.equal( + twitter_response_lines[i].strip()) + + #test iterating by line after a second request + response = requests.post(TWITTER_STREAMING_URL, data={'track': 'requests'}, + auth=('username', 'password'), stream=True) + + line_iter = response.iter_lines() + with in_time(0.01, 'Iterating by line is taking forever the second time ' + 'around!'): + for i in xrange(len(twitter_response_lines)): + expect(next(line_iter).strip()).to.equal( + twitter_response_lines[i].strip()) + + #test iterating by char + response = requests.post(TWITTER_STREAMING_URL, data={'track': 'requests'}, + auth=('username', 'password'), stream=True) + + twitter_expected_response_body = b''.join(twitter_response_lines) + with in_time(0.02, 'Iterating by char is taking forever!'): + twitter_body = b''.join(c for c in response.iter_content(chunk_size=1)) + + expect(twitter_body).to.equal(twitter_expected_response_body) + + #test iterating by chunks larger than the stream + + response = requests.post(TWITTER_STREAMING_URL, data={'track': 'requests'}, + auth=('username', 'password'), stream=True) + + with in_time(0.02, 'Iterating by large chunks is taking forever!'): + twitter_body = b''.join(c for c in + response.iter_content(chunk_size=1024)) + + expect(twitter_body).to.equal(twitter_expected_response_body) + + +@httprettified +def test_multiline(): + url = 'http://httpbin.org/post' + data = b'content=Im\r\na multiline\r\n\r\nsentence\r\n' + headers = { + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'Accept': 'text/plain', + } + HTTPretty.register_uri( + HTTPretty.POST, + url, + ) + response = requests.post(url, data=data, headers=headers) + + expect(response.status_code).to.equal(200) + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.path).to.equal('/post') + expect(HTTPretty.last_request.body).to.equal(data) + expect(HTTPretty.last_request.headers['content-length']).to.equal('37') + expect(HTTPretty.last_request.headers['content-type']).to.equal('application/x-www-form-urlencoded; charset=utf-8') + expect(len(HTTPretty.latest_requests)).to.equal(1) + + +@httprettified +def test_multipart(): + url = 'http://httpbin.org/post' + data = b'--xXXxXXyYYzzz\r\nContent-Disposition: form-data; name="content"\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 68\r\n\r\nAction: comment\nText: Comment with attach\nAttachment: x1.txt, x2.txt\r\n--xXXxXXyYYzzz\r\nContent-Disposition: form-data; name="attachment_2"; filename="x.txt"\r\nContent-Type: text/plain\r\nContent-Length: 4\r\n\r\nbye\n\r\n--xXXxXXyYYzzz\r\nContent-Disposition: form-data; name="attachment_1"; filename="x.txt"\r\nContent-Type: text/plain\r\nContent-Length: 4\r\n\r\nbye\n\r\n--xXXxXXyYYzzz--\r\n' + headers = {'Content-Length': '495', 'Content-Type': 'multipart/form-data; boundary=xXXxXXyYYzzz', 'Accept': 'text/plain'} + HTTPretty.register_uri( + HTTPretty.POST, + url, + ) + response = requests.post(url, data=data, headers=headers) + expect(response.status_code).to.equal(200) + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.path).to.equal('/post') + expect(HTTPretty.last_request.body).to.equal(data) + expect(HTTPretty.last_request.headers['content-length']).to.equal('495') + expect(HTTPretty.last_request.headers['content-type']).to.equal('multipart/form-data; boundary=xXXxXXyYYzzz') + expect(len(HTTPretty.latest_requests)).to.equal(1) + + +@httprettified +@within(two=microseconds) +def test_callback_response(now): + ("HTTPretty should call a callback function and set its return value as the body of the response" + " requests") + + def request_callback(request, uri, headers): + return [200, headers,"The {0} response from {1}".format(decode_utf8(request.method), uri)] + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + body=request_callback) + + response = requests.get('https://api.yahoo.com/test') + + expect(response.text).to.equal("The GET response from https://api.yahoo.com/test") + + HTTPretty.register_uri( + HTTPretty.POST, "https://api.yahoo.com/test_post", + body=request_callback) + + response = requests.post( + "https://api.yahoo.com/test_post", + {"username": "gabrielfalcao"} + ) + + expect(response.text).to.equal("The POST response from https://api.yahoo.com/test_post") + +@httprettified +@within(two=microseconds) +def test_callback_body_remains_callable_for_any_subsequent_requests(now): + ("HTTPretty should call a callback function more than one" + " requests") + + def request_callback(request, uri, headers): + return [200, headers,"The {0} response from {1}".format(decode_utf8(request.method), uri)] + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + body=request_callback) + + response = requests.get('https://api.yahoo.com/test') + expect(response.text).to.equal("The GET response from https://api.yahoo.com/test") + + response = requests.get('https://api.yahoo.com/test') + expect(response.text).to.equal("The GET response from https://api.yahoo.com/test") + +@httprettified +@within(two=microseconds) +def test_callback_setting_headers_and_status_response(now): + ("HTTPretty should call a callback function and uses it retur tuple as status code, headers and body" + " requests") + + def request_callback(request, uri, headers): + headers.update({'a':'b'}) + return [418,headers,"The {0} response from {1}".format(decode_utf8(request.method), uri)] + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + body=request_callback) + + response = requests.get('https://api.yahoo.com/test') + expect(response.text).to.equal("The GET response from https://api.yahoo.com/test") + expect(response.headers).to.have.key('a').being.equal("b") + expect(response.status_code).to.be(418) + + HTTPretty.register_uri( + HTTPretty.POST, "https://api.yahoo.com/test_post", + body=request_callback) + + response = requests.post( + "https://api.yahoo.com/test_post", + {"username": "gabrielfalcao"} + ) + + expect(response.text).to.equal("The POST response from https://api.yahoo.com/test_post") + expect(response.headers).to.have.key('a').being.equal("b") + expect(response.status_code).to.be(418) + +@httprettified +def test_httpretty_should_allow_registering_regexes_and_give_a_proper_match_to_the_callback(): + "HTTPretty should allow registering regexes with requests and giva a proper match to the callback" + + HTTPretty.register_uri( + HTTPretty.GET, + re.compile("https://api.yipit.com/v1/deal;brand=(?P<brand_name>\w+)"), + body=lambda method,uri,headers: [200,headers,uri] + ) + + response = requests.get('https://api.yipit.com/v1/deal;brand=gap?first_name=chuck&last_name=norris') + + expect(response.text).to.equal('https://api.yipit.com/v1/deal;brand=gap?first_name=chuck&last_name=norris') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/v1/deal;brand=gap?first_name=chuck&last_name=norris') + +@httprettified +def test_httpretty_should_allow_registering_regexes(): + "HTTPretty should allow registering regexes with requests" + + HTTPretty.register_uri( + HTTPretty.GET, + re.compile("https://api.yipit.com/v1/deal;brand=(?P<brand_name>\w+)"), + body="Found brand", + ) + + response = requests.get('https://api.yipit.com/v1/deal;brand=gap?first_name=chuck&last_name=norris' + ) + expect(response.text).to.equal('Found brand') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/v1/deal;brand=gap?first_name=chuck&last_name=norris') + + +@httprettified +def test_httpretty_provides_easy_access_to_querystrings_with_regexes(): + "HTTPretty should match regexes even if they have a different querystring" + + HTTPretty.register_uri( + HTTPretty.GET, + re.compile("https://api.yipit.com/v1/(?P<endpoint>\w+)/$"), + body="Find the best daily deals" + ) + + response = requests.get('https://api.yipit.com/v1/deals/?foo=bar&foo=baz&chuck=norris') + expect(response.text).to.equal("Find the best daily deals") + expect(HTTPretty.last_request.querystring).to.equal({ + 'foo': ['bar', 'baz'], + 'chuck': ['norris'], + }) + + +@httprettified +def test_httpretty_allows_to_chose_if_querystring_should_be_matched(): + "HTTPretty should provide a way to not match regexes that have a different querystring" + + HTTPretty.register_uri( + HTTPretty.GET, + re.compile("https://example.org/(?P<endpoint>\w+)/$"), + body="Nudge, nudge, wink, wink. Know what I mean?", + match_querystring=True + ) + + response = requests.get('https://example.org/what/') + expect(response.text).to.equal('Nudge, nudge, wink, wink. Know what I mean?') + try: + requests.get('https://example.org/what/?flying=coconuts') + raised = False + except requests.ConnectionError: + raised = True + + assert raised is True + + +@httprettified +def test_httpretty_should_allow_multiple_methods_for_the_same_uri(): + "HTTPretty should allow registering multiple methods for the same uri" + + url = 'http://test.com/test' + methods = ['GET', 'POST', 'PUT', 'OPTIONS'] + for method in methods: + HTTPretty.register_uri( + getattr(HTTPretty, method), + url, + method + ) + + for method in methods: + request_action = getattr(requests, method.lower()) + expect(request_action(url).text).to.equal(method) + + +@httprettified +def test_httpretty_should_allow_registering_regexes_with_streaming_responses(): + "HTTPretty should allow registering regexes with streaming responses" + import os + os.environ['DEBUG'] = 'true' + + def my_callback(request, url, headers): + request.body.should.equal('hithere') + return 200, headers, "Received" + + HTTPretty.register_uri( + HTTPretty.POST, + re.compile("https://api.yipit.com/v1/deal;brand=(?P<brand_name>\w+)"), + body=my_callback, + ) + + def gen(): + yield b'hi' + yield b'there' + + response = requests.post( + 'https://api.yipit.com/v1/deal;brand=gap?first_name=chuck&last_name=norris', + data=gen(), + ) + expect(response.content).to.equal("Received") + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.path).to.equal('/v1/deal;brand=gap?first_name=chuck&last_name=norris') + + +@httprettified +def test_httpretty_should_allow_multiple_responses_with_multiple_methods(): + "HTTPretty should allow multiple responses when binding multiple methods to the same uri" + + url = 'http://test.com/list' + + #add get responses + HTTPretty.register_uri(HTTPretty.GET, url, + responses=[HTTPretty.Response(body='a'), + HTTPretty.Response(body='b') + ] + ) + + #add post responses + HTTPretty.register_uri(HTTPretty.POST, url, + responses=[HTTPretty.Response(body='c'), + HTTPretty.Response(body='d') + ] + ) + + expect(requests.get(url).text).to.equal('a') + expect(requests.post(url).text).to.equal('c') + + expect(requests.get(url).text).to.equal('b') + expect(requests.get(url).text).to.equal('b') + expect(requests.get(url).text).to.equal('b') + + expect(requests.post(url).text).to.equal('d') + expect(requests.post(url).text).to.equal('d') + expect(requests.post(url).text).to.equal('d') + + +@httprettified +def test_httpretty_should_normalize_url_patching(): + "HTTPretty should normalize all url patching" + + HTTPretty.register_uri( + HTTPretty.GET, + "http://yipit.com/foo(bar)", + body="Find the best daily deals") + + response = requests.get('http://yipit.com/foo%28bar%29') + expect(response.text).to.equal('Find the best daily deals') + + +@httprettified +def test_lack_of_trailing_slash(): + ("HTTPretty should automatically append a slash to given urls") + url = 'http://www.youtube.com' + HTTPretty.register_uri(HTTPretty.GET, url, body='') + response = requests.get(url) + response.status_code.should.equal(200) + + +@httprettified +def test_unicode_querystrings(): + ("Querystrings should accept unicode characters") + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/login", + body="Find the best daily deals") + requests.get('http://yipit.com/login?user=Gabriel+Falcão') + expect(HTTPretty.last_request.querystring['user'][0]).should.be.equal('Gabriel Falcão') + + +@use_tornado_server +def test_recording_calls(): + ("HTTPretty should be able to record calls") + # Given a destination path: + destination = FIXTURE_FILE("recording-.json") + + # When I record some calls + with HTTPretty.record(destination): + requests.get(server_url("/foobar?name=Gabriel&age=25")) + requests.post(server_url("/foobar"), data=json.dumps({'test': '123'})) + + # Then the destination path should exist + os.path.exists(destination).should.be.true + + # And the contents should be json + raw = open(destination).read() + json.loads.when.called_with(raw).should_not.throw(ValueError) + + # And the contents should be expected + data = json.loads(raw) + data.should.be.a(list) + data.should.have.length_of(2) + + # And the responses should have the expected keys + response = data[0] + response.should.have.key("request").being.length_of(5) + response.should.have.key("response").being.length_of(3) + + response['request'].should.have.key("method").being.equal("GET") + response['request'].should.have.key("headers").being.a(dict) + response['request'].should.have.key("querystring").being.equal({ + "age": [ + "25" + ], + "name": [ + "Gabriel" + ] + }) + response['response'].should.have.key("status").being.equal(200) + response['response'].should.have.key("body").being.an(unicode) + response['response'].should.have.key("headers").being.a(dict) + response['response']["headers"].should.have.key("server").being.equal("TornadoServer/2.4") + + +def test_playing_calls(): + ("HTTPretty should be able to record calls") + # Given a destination path: + destination = FIXTURE_FILE("playback-1.json") + + # When I playback some previously recorded calls + with HTTPretty.playback(destination): + # And make the expected requests + response1 = requests.get(server_url("/foobar?name=Gabriel&age=25")) + response2 = requests.post(server_url("/foobar"), data=json.dumps({'test': '123'})) + + # Then the responses should be the expected + response1.json().should.equal({"foobar": {"age": "25", "name": "Gabriel"}}) + response2.json().should.equal({"foobar": {}}) + + +@httprettified +def test_py26_callback_response(): + ("HTTPretty should call a callback function *once* and set its return value" + " as the body of the response requests") + + from mock import Mock + + def _request_callback(request, uri, headers): + return [200, headers,"The {0} response from {1}".format(decode_utf8(request.method), uri)] + + request_callback = Mock() + request_callback.side_effect = _request_callback + + HTTPretty.register_uri( + HTTPretty.POST, "https://api.yahoo.com/test_post", + body=request_callback) + + response = requests.post( + "https://api.yahoo.com/test_post", + {"username": "gabrielfalcao"} + ) + os.environ['STOP'] = 'true' + expect(request_callback.call_count).equal(1) + + +import json + + +def hello(): + return json.dumps({ + 'href': 'http://foobar.com' + }) diff --git a/tests/functional/test_urllib2.py b/tests/functional/test_urllib2.py new file mode 100644 index 0000000..cb84f00 --- /dev/null +++ b/tests/functional/test_urllib2.py @@ -0,0 +1,337 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +try: + from urllib.request import urlopen + import urllib.request as urllib2 +except ImportError: + import urllib2 + urlopen = urllib2.urlopen + +from sure import * +from httpretty import HTTPretty, httprettified +from httpretty.core import decode_utf8 + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_mock_a_simple_get_with_urllib2_read(): + "HTTPretty should mock a simple GET with urllib2.read()" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + fd = urlopen('http://yipit.com') + got = fd.read() + fd.close() + + expect(got).to.equal(b'Find the best daily deals') + + +@httprettified +@within(two=microseconds) +def test_httpretty_provides_easy_access_to_querystrings(now): + "HTTPretty should provide an easy access to the querystring" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + fd = urllib2.urlopen('http://yipit.com/?foo=bar&foo=baz&chuck=norris') + fd.read() + fd.close() + + expect(HTTPretty.last_request.querystring).to.equal({ + 'foo': ['bar', 'baz'], + 'chuck': ['norris'], + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_mock_headers_urllib2(now): + "HTTPretty should mock basic headers with urllib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", + body="this is supposed to be the response", + status=201) + + request = urlopen('http://github.com') + + headers = dict(request.headers) + request.close() + + expect(request.code).to.equal(201) + expect(headers).to.equal({ + 'content-type': 'text/plain; charset=utf-8', + 'connection': 'close', + 'content-length': '35', + 'status': '201', + 'server': 'Python/HTTPretty', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_adding_and_overwritting_urllib2(now): + "HTTPretty should allow adding and overwritting headers with urllib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", + body="this is supposed to be the response", + adding_headers={ + 'Server': 'Apache', + 'Content-Length': '27', + 'Content-Type': 'application/json', + }) + + request = urlopen('http://github.com') + headers = dict(request.headers) + request.close() + + expect(request.code).to.equal(200) + expect(headers).to.equal({ + 'content-type': 'application/json', + 'connection': 'close', + 'content-length': '27', + 'status': '200', + 'server': 'Apache', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_forcing_headers_urllib2(): + "HTTPretty should allow forcing headers with urllib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", + body="this is supposed to be the response", + forcing_headers={ + 'Content-Type': 'application/xml', + }) + + request = urlopen('http://github.com') + headers = dict(request.headers) + request.close() + + expect(headers).to.equal({ + 'content-type': 'application/xml', + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_adding_and_overwritting_by_kwargs_u2(now): + "HTTPretty should allow adding and overwritting headers by " \ + "keyword args with urllib2" + + body = "this is supposed to be the response, indeed" + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", + body=body, + server='Apache', + content_length=len(body), + content_type='application/json') + + request = urlopen('http://github.com') + headers = dict(request.headers) + request.close() + + expect(request.code).to.equal(200) + expect(headers).to.equal({ + 'content-type': 'application/json', + 'connection': 'close', + 'content-length': str(len(body)), + 'status': '200', + 'server': 'Apache', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_support_a_list_of_successive_responses_urllib2(now): + "HTTPretty should support adding a list of successive " \ + "responses with urllib2" + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + responses=[ + HTTPretty.Response(body="first response", status=201), + HTTPretty.Response(body='second and last response', status=202), + ]) + + request1 = urlopen('https://api.yahoo.com/test') + body1 = request1.read() + request1.close() + + expect(request1.code).to.equal(201) + expect(body1).to.equal(b'first response') + + request2 = urlopen('https://api.yahoo.com/test') + body2 = request2.read() + request2.close() + expect(request2.code).to.equal(202) + expect(body2).to.equal(b'second and last response') + + request3 = urlopen('https://api.yahoo.com/test') + body3 = request3.read() + request3.close() + expect(request3.code).to.equal(202) + expect(body3).to.equal(b'second and last response') + + +@httprettified +@within(two=microseconds) +def test_can_inspect_last_request(now): + "HTTPretty.last_request is a mimetools.Message request from last match" + + HTTPretty.register_uri(HTTPretty.POST, "http://api.github.com/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + request = urllib2.Request( + 'http://api.github.com', + b'{"username": "gabrielfalcao"}', + { + 'content-type': 'text/json', + }, + ) + fd = urlopen(request) + got = fd.read() + fd.close() + + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.body).to.equal( + b'{"username": "gabrielfalcao"}', + ) + expect(HTTPretty.last_request.headers['content-type']).to.equal( + 'text/json', + ) + expect(got).to.equal(b'{"repositories": ["HTTPretty", "lettuce"]}') + + +@httprettified +@within(two=microseconds) +def test_can_inspect_last_request_with_ssl(now): + "HTTPretty.last_request is recorded even when mocking 'https' (SSL)" + + HTTPretty.register_uri(HTTPretty.POST, "https://secure.github.com/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + request = urllib2.Request( + 'https://secure.github.com', + b'{"username": "gabrielfalcao"}', + { + 'content-type': 'text/json', + }, + ) + fd = urlopen(request) + got = fd.read() + fd.close() + + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.body).to.equal( + b'{"username": "gabrielfalcao"}', + ) + expect(HTTPretty.last_request.headers['content-type']).to.equal( + 'text/json', + ) + expect(got).to.equal(b'{"repositories": ["HTTPretty", "lettuce"]}') + + +@httprettified +@within(two=microseconds) +def test_httpretty_ignores_querystrings_from_registered_uri(): + "HTTPretty should mock a simple GET with urllib2.read()" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/?id=123", + body="Find the best daily deals") + + fd = urlopen('http://yipit.com/?id=123') + got = fd.read() + fd.close() + + expect(got).to.equal(b'Find the best daily deals') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/?id=123') + + +@httprettified +@within(two=microseconds) +def test_callback_response(now): + ("HTTPretty should all a callback function to be set as the body with" + " urllib2") + + def request_callback(request, uri, headers): + return [200, headers, "The {0} response from {1}".format(decode_utf8(request.method), uri)] + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + body=request_callback) + + fd = urllib2.urlopen('https://api.yahoo.com/test') + got = fd.read() + fd.close() + + expect(got).to.equal(b"The GET response from https://api.yahoo.com/test") + + HTTPretty.register_uri( + HTTPretty.POST, "https://api.yahoo.com/test_post", + body=request_callback) + + request = urllib2.Request( + "https://api.yahoo.com/test_post", + b'{"username": "gabrielfalcao"}', + { + 'content-type': 'text/json', + }, + ) + fd = urllib2.urlopen(request) + got = fd.read() + fd.close() + + expect(got).to.equal(b"The POST response from https://api.yahoo.com/test_post") + + +@httprettified +def test_httpretty_should_allow_registering_regexes(): + "HTTPretty should allow registering regexes with urllib2" + + HTTPretty.register_uri( + HTTPretty.GET, + re.compile("https://api.yipit.com/v1/deal;brand=(?P<brand_name>\w+)"), + body="Found brand", + ) + + request = urllib2.Request( + "https://api.yipit.com/v1/deal;brand=GAP", + ) + fd = urllib2.urlopen(request) + got = fd.read() + fd.close() + + expect(got).to.equal(b"Found brand") diff --git a/tests/functional/testserver.py b/tests/functional/testserver.py new file mode 100644 index 0000000..db070ba --- /dev/null +++ b/tests/functional/testserver.py @@ -0,0 +1,171 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +import os +import sys + +try: + import io + StringIO = io.StringIO +except ImportError: + import StringIO + StringIO = StringIO.StringIO + +import time +import socket +from tornado.web import Application +from tornado.web import RequestHandler +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop +from multiprocessing import Process + +PY3 = sys.version_info[0] == 3 +if PY3: + text_type = str + byte_type = bytes +else: + text_type = unicode + byte_type = str + + +def utf8(s): + if isinstance(s, text_type): + s = s.encode('utf-8') + + return byte_type(s) + +true_socket = socket.socket + +PY3 = sys.version_info[0] == 3 + +if not PY3: + bytes = lambda s, *args: str(s) + + +class BubblesHandler(RequestHandler): + def get(self): + self.write(". o O 0 O o . o O 0 O o . o O 0 O o . o O 0 O o . o O 0 O o .") + + +class ComeHandler(RequestHandler): + def get(self): + self.write("<- HELLO WORLD ->") + + +class TornadoServer(object): + is_running = False + + def __init__(self, port): + self.port = int(port) + self.process = None + + @classmethod + def get_handlers(cls): + return Application([ + (r"/go-for-bubbles/?", BubblesHandler), + (r"/come-again/?", ComeHandler), + ]) + + def start(self): + def go(app, port, data={}): + from httpretty import HTTPretty + HTTPretty.disable() + http = HTTPServer(app) + http.listen(int(port)) + IOLoop.instance().start() + + app = self.get_handlers() + + data = {} + args = (app, self.port, data) + self.process = Process(target=go, args=args) + self.process.start() + time.sleep(0.4) + + def stop(self): + try: + os.kill(self.process.pid, 9) + except OSError: + self.process.terminate() + finally: + self.is_running = False + + +class TCPServer(object): + def __init__(self, port): + self.port = int(port) + + def start(self): + def go(port): + from httpretty import HTTPretty + HTTPretty.disable() + import socket + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(('localhost', port)) + s.listen(True) + conn, addr = s.accept() + + while True: + data = conn.recv(1024) + conn.send(b"RECEIVED: " + bytes(data)) + + conn.close() + + args = [self.port] + self.process = Process(target=go, args=args) + self.process.start() + time.sleep(0.4) + + def stop(self): + try: + os.kill(self.process.pid, 9) + except OSError: + self.process.terminate() + finally: + self.is_running = False + + +class TCPClient(object): + def __init__(self, port): + self.port = int(port) + self.sock = true_socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect(('localhost', self.port)) + + def send(self, what): + data = bytes(what, 'utf-8') + self.sock.sendall(data) + return self.sock.recv(len(data) + 11) + + def close(self): + try: + self.sock.close() + except socket.error: + pass # already closed + + def __del__(self): + self.close() diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..a5df75e --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1,25 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py new file mode 100644 index 0000000..e2551fb --- /dev/null +++ b/tests/unit/test_core.py @@ -0,0 +1,569 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import json +import errno +from datetime import datetime + +from mock import Mock, patch, call +from sure import expect + +from httpretty.core import HTTPrettyRequest, FakeSSLSocket, fakesock, httpretty + + +class SocketErrorStub(Exception): + def __init__(self, errno): + self.errno = errno + + +def test_request_stubs_internals(): + ("HTTPrettyRequest is a BaseHTTPRequestHandler that replaces " + "real socket file descriptors with in-memory ones") + + # Given a valid HTTP request header string + headers = "\r\n".join([ + 'POST /somewhere/?name=foo&age=bar HTTP/1.1', + 'Accept-Encoding: identity', + 'Host: github.com', + 'Content-Type: application/json', + 'Connection: close', + 'User-Agent: Python-urllib/2.7', + ]) + + # When I create a HTTPrettyRequest with an empty body + request = HTTPrettyRequest(headers, body='') + + # Then it should have parsed the headers + dict(request.headers).should.equal({ + 'accept-encoding': 'identity', + 'connection': 'close', + 'content-type': 'application/json', + 'host': 'github.com', + 'user-agent': 'Python-urllib/2.7' + }) + + # And the `rfile` should be a StringIO + request.should.have.property('rfile').being.a('StringIO.StringIO') + + # And the `wfile` should be a StringIO + request.should.have.property('wfile').being.a('StringIO.StringIO') + + # And the `method` should be available + request.should.have.property('method').being.equal('POST') + + + +def test_request_parse_querystring(): + ("HTTPrettyRequest#parse_querystring should parse unicode data") + + # Given a request string containing a unicode encoded querystring + + headers = "\r\n".join([ + 'POST /create?name=Gabriel+Falcão HTTP/1.1', + 'Content-Type: multipart/form-data', + ]) + + # When I create a HTTPrettyRequest with an empty body + request = HTTPrettyRequest(headers, body='') + + # Then it should have a parsed querystring + request.querystring.should.equal({'name': ['Gabriel Falcão']}) + + +def test_request_parse_body_when_it_is_application_json(): + ("HTTPrettyRequest#parse_request_body recognizes the " + "content-type `application/json` and parses it") + + # Given a request string containing a unicode encoded querystring + headers = "\r\n".join([ + 'POST /create HTTP/1.1', + 'Content-Type: application/json', + ]) + # And a valid json body + body = json.dumps({'name': 'Gabriel Falcão'}) + + # When I create a HTTPrettyRequest with that data + request = HTTPrettyRequest(headers, body) + + # Then it should have a parsed body + request.parsed_body.should.equal({'name': 'Gabriel Falcão'}) + + +def test_request_parse_body_when_it_is_text_json(): + ("HTTPrettyRequest#parse_request_body recognizes the " + "content-type `text/json` and parses it") + + # Given a request string containing a unicode encoded querystring + headers = "\r\n".join([ + 'POST /create HTTP/1.1', + 'Content-Type: text/json', + ]) + # And a valid json body + body = json.dumps({'name': 'Gabriel Falcão'}) + + # When I create a HTTPrettyRequest with that data + request = HTTPrettyRequest(headers, body) + + # Then it should have a parsed body + request.parsed_body.should.equal({'name': 'Gabriel Falcão'}) + + +def test_request_parse_body_when_it_is_urlencoded(): + ("HTTPrettyRequest#parse_request_body recognizes the " + "content-type `application/x-www-form-urlencoded` and parses it") + + # Given a request string containing a unicode encoded querystring + headers = "\r\n".join([ + 'POST /create HTTP/1.1', + 'Content-Type: application/x-www-form-urlencoded', + ]) + # And a valid urlencoded body + body = "name=Gabriel+Falcão&age=25&projects=httpretty&projects=sure&projects=lettuce" + + # When I create a HTTPrettyRequest with that data + request = HTTPrettyRequest(headers, body) + + # Then it should have a parsed body + request.parsed_body.should.equal({ + 'name': ['Gabriel Falcão'], + 'age': ["25"], + 'projects': ["httpretty", "sure", "lettuce"] + }) + + +def test_request_parse_body_when_unrecognized(): + ("HTTPrettyRequest#parse_request_body returns the value as " + "is if the Content-Type is not recognized") + + # Given a request string containing a unicode encoded querystring + headers = "\r\n".join([ + 'POST /create HTTP/1.1', + 'Content-Type: whatever', + ]) + # And a valid urlencoded body + body = "foobar:\nlalala" + + # When I create a HTTPrettyRequest with that data + request = HTTPrettyRequest(headers, body) + + # Then it should have a parsed body + request.parsed_body.should.equal("foobar:\nlalala") + + +def test_request_string_representation(): + ("HTTPrettyRequest should have a debug-friendly " + "string representation") + + # Given a request string containing a unicode encoded querystring + headers = "\r\n".join([ + 'POST /create HTTP/1.1', + 'Content-Type: JPEG-baby', + ]) + # And a valid urlencoded body + body = "foobar:\nlalala" + + # When I create a HTTPrettyRequest with that data + request = HTTPrettyRequest(headers, body) + + # Then its string representation should show the headers and the body + str(request).should.equal('<HTTPrettyRequest("JPEG-baby", total_headers=1, body_length=14)>') + + +def test_fake_ssl_socket_proxies_its_ow_socket(): + ("FakeSSLSocket is a simpel wrapper around its own socket, " + "which was designed to be a HTTPretty fake socket") + + # Given a sentinel mock object + socket = Mock() + + # And a FakeSSLSocket wrapping it + ssl = FakeSSLSocket(socket) + + # When I make a method call + ssl.send("FOO") + + # Then it should bypass any method calls to its own socket + socket.send.assert_called_once_with("FOO") + + +@patch('httpretty.core.datetime') +def test_fakesock_socket_getpeercert(dt): + ("fakesock.socket#getpeercert should return a hardcoded fake certificate") + # Background: + dt.now.return_value = datetime(2013, 10, 4, 4, 20, 0) + + # Given a fake socket instance + socket = fakesock.socket() + + # And that it's bound to some host and port + socket.connect(('somewhere.com', 80)) + + # When I retrieve the peer certificate + certificate = socket.getpeercert() + + # Then it should return a hardcoded value + certificate.should.equal({ + u'notAfter': 'Sep 29 04:20:00 GMT', + u'subject': ( + ((u'organizationName', u'*.somewhere.com'),), + ((u'organizationalUnitName', u'Domain Control Validated'),), + ((u'commonName', u'*.somewhere.com'),)), + u'subjectAltName': ( + (u'DNS', u'*somewhere.com'), + (u'DNS', u'somewhere.com'), + (u'DNS', u'*') + ) + }) + + +def test_fakesock_socket_ssl(): + ("fakesock.socket#ssl should take a socket instance and return itself") + # Given a fake socket instance + socket = fakesock.socket() + + # And a stubbed socket sentinel + sentinel = Mock() + + # When I call `ssl` on that mock + result = socket.ssl(sentinel) + + # Then it should have returned its first argument + result.should.equal(sentinel) + + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_connect_fallback(POTENTIAL_HTTP_PORTS, old_socket): + ("fakesock.socket#connect should open a real connection if the " + "given port is not a potential http port") + # Background: the potential http ports are 80 and 443 + POTENTIAL_HTTP_PORTS.__contains__.side_effect = lambda other: int(other) in (80, 443) + + # Given a fake socket instance + socket = fakesock.socket() + + # When it is connected to a remote server in a port that isn't 80 nor 443 + socket.connect(('somewhere.com', 42)) + + # Then it should have open a real connection in the background + old_socket.return_value.connect.assert_called_once_with(('somewhere.com', 42)) + + # And _closed is set to False + socket._closed.should.be.false + + +@patch('httpretty.core.old_socket') +def test_fakesock_socket_close(old_socket): + ("fakesock.socket#close should close the actual socket in case " + "it's not http and _closed is False") + # Given a fake socket instance that is synthetically open + socket = fakesock.socket() + socket._closed = False + + # When I close it + socket.close() + + # Then its real socket should have been closed + old_socket.return_value.close.assert_called_once_with() + + # And _closed is set to True + socket._closed.should.be.true + + +@patch('httpretty.core.old_socket') +def test_fakesock_socket_makefile(old_socket): + ("fakesock.socket#makefile should set the mode, " + "bufsize and return its mocked file descriptor") + + # Given a fake socket that has a mocked Entry associated with it + socket = fakesock.socket() + socket._entry = Mock() + + # When I call makefile() + fd = socket.makefile(mode='rw', bufsize=512) + + # Then it should have returned the socket's own filedescriptor + expect(fd).to.equal(socket.fd) + # And the mode should have been set in the socket instance + socket._mode.should.equal('rw') + # And the bufsize should have been set in the socket instance + socket._bufsize.should.equal(512) + + # And the entry should have been filled with that filedescriptor + socket._entry.fill_filekind.assert_called_once_with(fd) + + +@patch('httpretty.core.old_socket') +def test_fakesock_socket_real_sendall(old_socket): + ("fakesock.socket#real_sendall sends data and buffers " + "the response in the file descriptor") + # Background: the real socket will stop returning bytes after the + # first call + real_socket = old_socket.return_value + real_socket.recv.side_effect = ['response from server', ""] + + # Given a fake socket + socket = fakesock.socket() + + # When I call real_sendall with data, some args and kwargs + socket.real_sendall("SOMEDATA", 'some extra args...', foo='bar') + + # Then it should have called sendall in the real socket + real_socket.sendall.assert_called_once_with("SOMEDATA", 'some extra args...', foo='bar') + + # And the timeout was set to 0 + real_socket.settimeout.assert_called_once_with(0) + + # And recv was called with the bufsize + real_socket.recv.assert_has_calls([ + call(16), + call(16), + ]) + + # And the buffer should contain the data from the server + socket.fd.getvalue().should.equal("response from server") + + # And connect was never called + real_socket.connect.called.should.be.false + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.socket') +def test_fakesock_socket_real_sendall_continue_eagain(socket, old_socket): + ("fakesock.socket#real_sendall should continue if the socket error was EAGAIN") + socket.error = SocketErrorStub + # Background: the real socket will stop returning bytes after the + # first call + real_socket = old_socket.return_value + real_socket.recv.side_effect = [SocketErrorStub(errno.EAGAIN), 'after error', ""] + + # Given a fake socket + socket = fakesock.socket() + + + # When I call real_sendall with data, some args and kwargs + socket.real_sendall("SOMEDATA", 'some extra args...', foo='bar') + + # Then it should have called sendall in the real socket + real_socket.sendall.assert_called_once_with("SOMEDATA", 'some extra args...', foo='bar') + + # And the timeout was set to 0 + real_socket.settimeout.assert_called_once_with(0) + + # And recv was called with the bufsize + real_socket.recv.assert_has_calls([ + call(16), + call(16), + ]) + + # And the buffer should contain the data from the server + socket.fd.getvalue().should.equal("after error") + + # And connect was never called + real_socket.connect.called.should.be.false + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.socket') +def test_fakesock_socket_real_sendall_socket_error(socket, old_socket): + ("fakesock.socket#real_sendall should continue if the socket error was EAGAIN") + socket.error = SocketErrorStub + # Background: the real socket will stop returning bytes after the + # first call + real_socket = old_socket.return_value + real_socket.recv.side_effect = [SocketErrorStub(42), 'after error', ""] + + # Given a fake socket + socket = fakesock.socket() + + # When I call real_sendall with data, some args and kwargs + socket.real_sendall("SOMEDATA", 'some extra args...', foo='bar') + + # Then it should have called sendall in the real socket + real_socket.sendall.assert_called_once_with("SOMEDATA", 'some extra args...', foo='bar') + + # And the timeout was set to 0 + real_socket.settimeout.assert_called_once_with(0) + + # And recv was called with the bufsize + real_socket.recv.assert_called_once_with(16) + + # And the buffer should contain the data from the server + socket.fd.getvalue().should.equal("") + + # And connect was never called + real_socket.connect.called.should.be.false + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_real_sendall_when_http(POTENTIAL_HTTP_PORTS, old_socket): + ("fakesock.socket#real_sendall should connect before sending data") + # Background: the real socket will stop returning bytes after the + # first call + real_socket = old_socket.return_value + real_socket.recv.side_effect = ['response from foobar :)', ""] + + # And the potential http port is 4000 + POTENTIAL_HTTP_PORTS.__contains__.side_effect = lambda other: int(other) == 4000 + + # Given a fake socket + socket = fakesock.socket() + + # When I call connect to a server in a port that is considered HTTP + socket.connect(('foobar.com', 4000)) + + # And send some data + socket.real_sendall("SOMEDATA") + + # Then connect should have been called + real_socket.connect.assert_called_once_with(('foobar.com', 4000)) + + # And the timeout was set to 0 + real_socket.settimeout.assert_called_once_with(0) + + # And recv was called with the bufsize + real_socket.recv.assert_has_calls([ + call(16), + call(16), + ]) + + # And the buffer should contain the data from the server + socket.fd.getvalue().should.equal("response from foobar :)") + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.httpretty') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_sendall_with_valid_requestline(POTENTIAL_HTTP_PORTS, httpretty, old_socket): + ("fakesock.socket#sendall should create an entry if it's given a valid request line") + matcher = Mock() + info = Mock() + httpretty.match_uriinfo.return_value = (matcher, info) + httpretty.register_uri(httpretty.GET, 'http://foo.com/foobar') + + # Background: + # using a subclass of socket that mocks out real_sendall + class MySocket(fakesock.socket): + def real_sendall(self, data, *args, **kw): + raise AssertionError('should never call this...') + + # Given an instance of that socket + socket = MySocket() + + # And that is is considered http + socket.connect(('foo.com', 80)) + + # When I try to send data + socket.sendall("GET /foobar HTTP/1.1\r\nContent-Type: application/json\r\n\r\n") + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.httpretty') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_sendall_with_valid_requestline(POTENTIAL_HTTP_PORTS, httpretty, old_socket): + ("fakesock.socket#sendall should create an entry if it's given a valid request line") + matcher = Mock() + info = Mock() + httpretty.match_uriinfo.return_value = (matcher, info) + httpretty.register_uri(httpretty.GET, 'http://foo.com/foobar') + + # Background: + # using a subclass of socket that mocks out real_sendall + class MySocket(fakesock.socket): + def real_sendall(self, data, *args, **kw): + raise AssertionError('should never call this...') + + # Given an instance of that socket + socket = MySocket() + + # And that is is considered http + socket.connect(('foo.com', 80)) + + # When I try to send data + socket.sendall("GET /foobar HTTP/1.1\r\nContent-Type: application/json\r\n\r\n") + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_sendall_with_body_data_no_entry(POTENTIAL_HTTP_PORTS, old_socket): + ("fakesock.socket#sendall should call real_sendall when not parsing headers and there is no entry") + # Background: + # Using a subclass of socket that mocks out real_sendall + class MySocket(fakesock.socket): + def real_sendall(self, data): + data.should.equal('BLABLABLABLA') + return 'cool' + + # Given an instance of that socket + socket = MySocket() + socket._entry = None + + # And that is is considered http + socket.connect(('foo.com', 80)) + + # When I try to send data + result = socket.sendall("BLABLABLABLA") + + # Then the result should be the return value from real_sendall + result.should.equal('cool') + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_sendall_with_body_data_with_entry(POTENTIAL_HTTP_PORTS, old_socket): + ("fakesock.socket#sendall should call real_sendall when not ") + # Background: + # Using a subclass of socket that mocks out real_sendall + class MySocket(fakesock.socket): + def real_sendall(self, data): + raise AssertionError('should have never been called') + # Using a mocked entry + entry = Mock() + entry.request.headers = {} + entry.request.body = '' + + # Given an instance of that socket + socket = MySocket() + socket._entry = entry + + + # And that is is considered http + socket.connect(('foo.com', 80)) + + # When I try to send data + socket.sendall("BLABLABLABLA") + + # Then the entry should have that body + entry.request.body.should.equal('BLABLABLABLA') + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_sendall_with_body_data_with_chunked_entry(POTENTIAL_HTTP_PORTS, old_socket): + ("fakesock.socket#sendall should call real_sendall when not ") + # Background: + # Using a subclass of socket that mocks out real_sendall + class MySocket(fakesock.socket): + def real_sendall(self, data): + raise AssertionError('should have never been called') + # Using a mocked entry + entry = Mock() + entry.request.headers = { + 'transfer-encoding': 'chunked', + } + entry.request.body = '' + + # Given an instance of that socket + socket = MySocket() + socket._entry = entry + + # And that is is considered http + socket.connect(('foo.com', 80)) + + # When I try to send data + socket.sendall("BLABLABLABLA") + + # Then the entry should have that body + httpretty.last_request.body.should.equal('BLABLABLABLA') diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py new file mode 100644 index 0000000..7ea5fe1 --- /dev/null +++ b/tests/unit/test_http.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from httpretty.http import parse_requestline + + +def test_parse_request_line_connect(): + ("parse_requestline should parse the CONNECT method appropriately") + + # Given a valid request line string that has the CONNECT method + line = "CONNECT / HTTP/1.1" + + # When I parse it + result = parse_requestline(line) + + # Then it should return a tuple + result.should.equal(("CONNECT", "/", "1.1")) diff --git a/tests/unit/test_httpretty.py b/tests/unit/test_httpretty.py new file mode 100644 index 0000000..420aaee --- /dev/null +++ b/tests/unit/test_httpretty.py @@ -0,0 +1,386 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> Gabriel Falcão <gabriel@nacaolivre.org> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals +import json +from sure import expect +from httpretty import HTTPretty, HTTPrettyError, core +from httpretty.core import URIInfo, BaseClass, Entry, FakeSockFile, HTTPrettyRequest +from httpretty.http import STATUSES + +try: + from mock import MagicMock +except ImportError: + from unittest.mock import MagicMock + +TEST_HEADER = """ +GET /test/test.html HTTP/1.1 +Host: www.host1.com:80 +Content-Type: %(content_type)s +""" + + +def test_httpretty_should_raise_proper_exception_on_inconsistent_length(): + "HTTPretty should raise proper exception on inconsistent Content-Length / "\ + "registered response body" + expect(HTTPretty.register_uri).when.called_with( + HTTPretty.GET, + "http://github.com/gabrielfalcao", + body="that's me!", + adding_headers={ + 'Content-Length': '999' + } + ).to.throw( + HTTPrettyError, + 'HTTPretty got inconsistent parameters. The header Content-Length you registered expects size "999" ' + 'but the body you registered for that has actually length "10".' + ) + + +def test_httpretty_should_raise_on_socket_send_when_uri_registered(): + """HTTPretty should raise a RuntimeError when the fakesocket is used in + an invalid usage. + """ + import socket + HTTPretty.enable() + + HTTPretty.register_uri(HTTPretty.GET, + 'http://127.0.0.1:5000') + expect(core.POTENTIAL_HTTP_PORTS).to.be.equal(set([80, 443, 5000])) + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('127.0.0.1', 5000)) + expect(sock.send).when.called_with(b'whatever').to.throw(RuntimeError) + sock.close() + + # restore the previous value + core.POTENTIAL_HTTP_PORTS.remove(5000) + HTTPretty.reset() + HTTPretty.disable() + + +def test_httpretty_should_not_raise_on_socket_send_when_uri_not_registered(): + """HTTPretty should not raise a RuntimeError when the fakesocket is used in + an invalid usage. + """ + import socket + HTTPretty.enable() + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) + sock.setblocking(0) + expect(sock.sendto).when.called_with(b'whatever', + ('127.0.0.1', 53) + ).should_not.throw(RuntimeError) + + sock.close() + HTTPretty.reset() + HTTPretty.disable() + + +def test_does_not_have_last_request_by_default(): + 'HTTPretty.last_request is a dummy object by default' + HTTPretty.reset() + + expect(HTTPretty.last_request.headers).to.be.empty + expect(HTTPretty.last_request.body).to.be.empty + + +def test_status_codes(): + "HTTPretty supports N status codes" + + expect(STATUSES).to.equal({ + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 306: "Switch Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request a Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", + 420: "Enhance Your Calm", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 424: "Method Failure", + 425: "Unordered Collection", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 444: "No Response", + 449: "Retry With", + 450: "Blocked by Windows Parental Controls", + 451: "Unavailable For Legal Reasons", + 451: "Redirect", + 494: "Request Header Too Large", + 495: "Cert Error", + 496: "No Cert", + 497: "HTTP to HTTPS", + 499: "Client Closed Request", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 509: "Bandwidth Limit Exceeded", + 510: "Not Extended", + 511: "Network Authentication Required", + 598: "Network read timeout error", + 599: "Network connect timeout error", + }) + +def test_uri_info_full_url(): + uri_info = URIInfo( + username='johhny', + password='password', + hostname=b'google.com', + port=80, + path=b'/', + query=b'foo=bar&baz=test', + fragment='', + scheme='', + ) + + expect(uri_info.full_url()).to.equal( + "http://johhny:password@google.com/?foo=bar&baz=test" + ) + + expect(uri_info.full_url(use_querystring=False)).to.equal( + "http://johhny:password@google.com/" + ) + +def test_uri_info_eq_ignores_case(): + """Test that URIInfo.__eq__ method ignores case for + hostname matching. + """ + uri_info_uppercase = URIInfo( + username='johhny', + password='password', + hostname=b'GOOGLE.COM', + port=80, + path=b'/', + query=b'foo=bar&baz=test', + fragment='', + scheme='', + ) + uri_info_lowercase = URIInfo( + username='johhny', + password='password', + hostname=b'google.com', + port=80, + path=b'/', + query=b'foo=bar&baz=test', + fragment='', + scheme='', + ) + expect(uri_info_uppercase).to.equal(uri_info_lowercase) + +def test_global_boolean_enabled(): + expect(HTTPretty.is_enabled()).to.be.falsy + HTTPretty.enable() + expect(HTTPretty.is_enabled()).to.be.truthy + HTTPretty.disable() + expect(HTTPretty.is_enabled()).to.be.falsy + + +def test_py3kobject_implements_valid__repr__based_on__str__(): + class MyObject(BaseClass): + def __str__(self): + return 'hi' + + myobj = MyObject() + expect(repr(myobj)).to.be.equal('hi') + + +def test_Entry_class_normalizes_headers(): + entry = Entry(HTTPretty.GET, 'http://example.com', 'example', + host='example.com', cache_control='no-cache', x_forward_for='proxy') + + expect(entry.adding_headers).to.equal({ + 'Host':'example.com', + 'Cache-Control':'no-cache', + 'X-Forward-For':'proxy' + }) + + +def test_Entry_class_counts_multibyte_characters_in_bytes(): + entry = Entry(HTTPretty.GET, 'http://example.com', 'こんにちは') + buf = FakeSockFile() + entry.fill_filekind(buf) + response = buf.getvalue() + expect(b'content-length: 15\n').to.be.within(response) + + +def test_fake_socket_passes_through_setblocking(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.setblocking).called_with(0).should_not.throw(AttributeError) + s.truesock.setblocking.assert_called_with(0) + +def test_fake_socket_passes_through_fileno(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.fileno).called_with().should_not.throw(AttributeError) + s.truesock.fileno.assert_called_with() + + +def test_fake_socket_passes_through_getsockopt(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.getsockopt).called_with(socket.SOL_SOCKET, 1).should_not.throw(AttributeError) + s.truesock.getsockopt.assert_called_with(socket.SOL_SOCKET, 1) + +def test_fake_socket_passes_through_bind(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.bind).called_with().should_not.throw(AttributeError) + s.truesock.bind.assert_called_with() + +def test_fake_socket_passes_through_connect_ex(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.connect_ex).called_with().should_not.throw(AttributeError) + s.truesock.connect_ex.assert_called_with() + +def test_fake_socket_passes_through_listen(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.listen).called_with().should_not.throw(AttributeError) + s.truesock.listen.assert_called_with() + +def test_fake_socket_passes_through_getpeername(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.getpeername).called_with().should_not.throw(AttributeError) + s.truesock.getpeername.assert_called_with() + +def test_fake_socket_passes_through_getsockname(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.getsockname).called_with().should_not.throw(AttributeError) + s.truesock.getsockname.assert_called_with() + +def test_fake_socket_passes_through_gettimeout(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.gettimeout).called_with().should_not.throw(AttributeError) + s.truesock.gettimeout.assert_called_with() + +def test_fake_socket_passes_through_shutdown(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.shutdown).called_with(socket.SHUT_RD).should_not.throw(AttributeError) + s.truesock.shutdown.assert_called_with(socket.SHUT_RD) + + +def test_HTTPrettyRequest_json_body(): + """ A content-type of application/json should parse a valid json body """ + header = TEST_HEADER % {'content_type': 'application/json'} + test_dict = {'hello': 'world'} + request = HTTPrettyRequest(header, json.dumps(test_dict)) + expect(request.parsed_body).to.equal(test_dict) + + +def test_HTTPrettyRequest_invalid_json_body(): + """ A content-type of application/json with an invalid json body should return the content unaltered """ + header = TEST_HEADER % {'content_type': 'application/json'} + invalid_json = u"{'hello', 'world','thisstringdoesntstops}" + request = HTTPrettyRequest(header, invalid_json) + expect(request.parsed_body).to.equal(invalid_json) + + +def test_HTTPrettyRequest_queryparam(): + """ A content-type of x-www-form-urlencoded with a valid queryparam body should return parsed content """ + header = TEST_HEADER % {'content_type': 'application/x-www-form-urlencoded'} + valid_queryparam = u"hello=world&this=isavalidquerystring" + valid_results = {'hello': ['world'], 'this': ['isavalidquerystring']} + request = HTTPrettyRequest(header, valid_queryparam) + expect(request.parsed_body).to.equal(valid_results) + + +def test_HTTPrettyRequest_arbitrarypost(): + """ A non-handled content type request's post body should return the content unaltered """ + header = TEST_HEADER % {'content_type': 'thisis/notarealcontenttype'} + gibberish_body = "1234567890!@#$%^&*()" + request = HTTPrettyRequest(header, gibberish_body) + expect(request.parsed_body).to.equal(gibberish_body) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py new file mode 100644 index 0000000..efa977c --- /dev/null +++ b/tests/unit/test_main.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from mock import patch +import httpretty + + +@patch('httpretty.httpretty') +def test_last_request(original): + ("httpretty.last_request() should return httpretty.core.last_request") + + httpretty.last_request().should.equal(original.last_request) diff --git a/theme/assets/README.txt b/theme/assets/README.txt new file mode 100644 index 0000000..e5b5b41 --- /dev/null +++ b/theme/assets/README.txt @@ -0,0 +1,3 @@ +A Pen created at CodePen.io. You can find this one at http://codepen.io/gabrielfalcao/pen/Esdub. + +
\ No newline at end of file diff --git a/theme/assets/css/github.css b/theme/assets/css/github.css new file mode 100644 index 0000000..dc60655 --- /dev/null +++ b/theme/assets/css/github.css @@ -0,0 +1,61 @@ +.hll { background-color: #ffffcc } +.c { color: #999988; font-style: italic } /* Comment */ +.err { color: #a61717; background-color: #e3d2d2 } /* Error */ +.k { color: #000000; font-weight: bold } /* Keyword */ +.o { color: #000000; font-weight: bold } /* Operator */ +.cm { color: #999988; font-style: italic } /* Comment.Multiline */ +.cp { color: #999999; font-weight: bold; font-style: italic } /* Comment.Preproc */ +.c1 { color: #999988; font-style: italic } /* Comment.Single */ +.cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ +.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ +.ge { color: #000000; font-style: italic } /* Generic.Emph */ +.gr { color: #aa0000 } /* Generic.Error */ +.gh { color: #999999 } /* Generic.Heading */ +.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ +.go { color: #888888 } /* Generic.Output */ +.gp { color: #555555 } /* Generic.Prompt */ +.gs { font-weight: bold } /* Generic.Strong */ +.gu { color: #aaaaaa } /* Generic.Subheading */ +.gt { color: #aa0000 } /* Generic.Traceback */ +.kc { color: #000000; font-weight: bold } /* Keyword.Constant */ +.kd { color: #000000; font-weight: bold } /* Keyword.Declaration */ +.kn { color: #000000; font-weight: bold } /* Keyword.Namespace */ +.kp { color: #000000; font-weight: bold } /* Keyword.Pseudo */ +.kr { color: #000000; font-weight: bold } /* Keyword.Reserved */ +.kt { color: #445588; font-weight: bold } /* Keyword.Type */ +.m { color: #009999 } /* Literal.Number */ +.s { color: #d01040 } /* Literal.String */ +.na { color: #008080 } /* Name.Attribute */ +.nb { color: #0086B3 } /* Name.Builtin */ +.nc { color: #445588; font-weight: bold } /* Name.Class */ +.no { color: #008080 } /* Name.Constant */ +.nd { color: #3c5d5d; font-weight: bold } /* Name.Decorator */ +.ni { color: #800080 } /* Name.Entity */ +.ne { color: #990000; font-weight: bold } /* Name.Exception */ +.nf { color: #990000; font-weight: bold } /* Name.Function */ +.nl { color: #990000; font-weight: bold } /* Name.Label */ +.nn { color: #555555 } /* Name.Namespace */ +.nt { color: #000080 } /* Name.Tag */ +.nv { color: #008080 } /* Name.Variable */ +.ow { color: #000000; font-weight: bold } /* Operator.Word */ +.w { color: #bbbbbb } /* Text.Whitespace */ +.mf { color: #009999 } /* Literal.Number.Float */ +.mh { color: #009999 } /* Literal.Number.Hex */ +.mi { color: #009999 } /* Literal.Number.Integer */ +.mo { color: #009999 } /* Literal.Number.Oct */ +.sb { color: #d01040 } /* Literal.String.Backtick */ +.sc { color: #d01040 } /* Literal.String.Char */ +.sd { color: #d01040 } /* Literal.String.Doc */ +.s2 { color: #d01040 } /* Literal.String.Double */ +.se { color: #d01040 } /* Literal.String.Escape */ +.sh { color: #d01040 } /* Literal.String.Heredoc */ +.si { color: #d01040 } /* Literal.String.Interpol */ +.sx { color: #d01040 } /* Literal.String.Other */ +.sr { color: #009926 } /* Literal.String.Regex */ +.s1 { color: #d01040 } /* Literal.String.Single */ +.ss { color: #990073 } /* Literal.String.Symbol */ +.bp { color: #999999 } /* Name.Builtin.Pseudo */ +.vc { color: #008080 } /* Name.Variable.Class */ +.vg { color: #008080 } /* Name.Variable.Global */ +.vi { color: #008080 } /* Name.Variable.Instance */ +.il { color: #009999 } /* Literal.Number.Integer.Long */ diff --git a/theme/assets/css/style.css b/theme/assets/css/style.css new file mode 100644 index 0000000..17a3813 --- /dev/null +++ b/theme/assets/css/style.css @@ -0,0 +1,68 @@ +html { + background: #faf9f4; +} +body { + background: transparent; + top: 0; + bottom: 0; + left: 0; + right: 0; +} +.logo { + padding-top: 30px; +} +.logo h1 { + font-size: 60px; +} +.logo h1 small { + font-size: 30px; +} +*, +body, +html, +.article, +h1, +h2, +h3, +h4, +h5, +h6, +p, +div, +span, +strong, +a, +li, +ul, +ol, +nav { + font-family: "Montserrat"; +} +.httpretty-navbar .uk-navbar-brand { + font-size: 28px; + padding-top: 4px; +} +.httpretty-navbar .uk-navbar-brand small { + font-size: 14px; +} +a.uk-button-primary { + color: white; +} + + +a { + color: #3973A6; +} + +code, code *, pre, pre * { + font-family: "Monaco", "Inconsolata", "monospace"; +} + +div.highlight pre { + box-shadow: inset 0px 1px 2px 1px rgba(80, 80, 80, .15); +} + + +#docs-body { + padding-top: 120px; +} diff --git a/theme/assets/js/prefixfree.min.js b/theme/assets/js/prefixfree.min.js new file mode 100644 index 0000000..9512edd --- /dev/null +++ b/theme/assets/js/prefixfree.min.js @@ -0,0 +1,492 @@ +/** + * StyleFix 1.0.3 & PrefixFree 1.0.7 + * @author Lea Verou + * MIT license + */ + +(function(){ + +if(!window.addEventListener) { + return; +} + +var self = window.StyleFix = { + link: function(link) { + try { + // Ignore stylesheets with data-noprefix attribute as well as alternate stylesheets + if(link.rel !== 'stylesheet' || link.hasAttribute('data-noprefix')) { + return; + } + } + catch(e) { + return; + } + + var url = link.href || link.getAttribute('data-href'), + base = url.replace(/[^\/]+$/, ''), + base_scheme = (/^[a-z]{3,10}:/.exec(base) || [''])[0], + base_domain = (/^[a-z]{3,10}:\/\/[^\/]+/.exec(base) || [''])[0], + base_query = /^([^?]*)\??/.exec(url)[1], + parent = link.parentNode, + xhr = new XMLHttpRequest(), + process; + + xhr.onreadystatechange = function() { + if(xhr.readyState === 4) { + process(); + } + }; + + process = function() { + var css = xhr.responseText; + + if(css && link.parentNode && (!xhr.status || xhr.status < 400 || xhr.status > 600)) { + css = self.fix(css, true, link); + + // Convert relative URLs to absolute, if needed + if(base) { + css = css.replace(/url\(\s*?((?:"|')?)(.+?)\1\s*?\)/gi, function($0, quote, url) { + if(/^([a-z]{3,10}:|#)/i.test(url)) { // Absolute & or hash-relative + return $0; + } + else if(/^\/\//.test(url)) { // Scheme-relative + // May contain sequences like /../ and /./ but those DO work + return 'url("' + base_scheme + url + '")'; + } + else if(/^\//.test(url)) { // Domain-relative + return 'url("' + base_domain + url + '")'; + } + else if(/^\?/.test(url)) { // Query-relative + return 'url("' + base_query + url + '")'; + } + else { + // Path-relative + return 'url("' + base + url + '")'; + } + }); + + // behavior URLs shoudn’t be converted (Issue #19) + // base should be escaped before added to RegExp (Issue #81) + var escaped_base = base.replace(/([\\\^\$*+[\]?{}.=!:(|)])/g,"\\$1"); + css = css.replace(RegExp('\\b(behavior:\\s*?url\\(\'?"?)' + escaped_base, 'gi'), '$1'); + } + + var style = document.createElement('style'); + style.textContent = css; + style.media = link.media; + style.disabled = link.disabled; + style.setAttribute('data-href', link.getAttribute('href')); + + parent.insertBefore(style, link); + parent.removeChild(link); + + style.media = link.media; // Duplicate is intentional. See issue #31 + } + }; + + try { + xhr.open('GET', url); + xhr.send(null); + } catch (e) { + // Fallback to XDomainRequest if available + if (typeof XDomainRequest != "undefined") { + xhr = new XDomainRequest(); + xhr.onerror = xhr.onprogress = function() {}; + xhr.onload = process; + xhr.open("GET", url); + xhr.send(null); + } + } + + link.setAttribute('data-inprogress', ''); + }, + + styleElement: function(style) { + if (style.hasAttribute('data-noprefix')) { + return; + } + var disabled = style.disabled; + + style.textContent = self.fix(style.textContent, true, style); + + style.disabled = disabled; + }, + + styleAttribute: function(element) { + var css = element.getAttribute('style'); + + css = self.fix(css, false, element); + + element.setAttribute('style', css); + }, + + process: function() { + // Linked stylesheets + $('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link); + + // Inline stylesheets + $('style').forEach(StyleFix.styleElement); + + // Inline styles + $('[style]').forEach(StyleFix.styleAttribute); + }, + + register: function(fixer, index) { + (self.fixers = self.fixers || []) + .splice(index === undefined? self.fixers.length : index, 0, fixer); + }, + + fix: function(css, raw, element) { + for(var i=0; i<self.fixers.length; i++) { + css = self.fixers[i](css, raw, element) || css; + } + + return css; + }, + + camelCase: function(str) { + return str.replace(/-([a-z])/g, function($0, $1) { return $1.toUpperCase(); }).replace('-',''); + }, + + deCamelCase: function(str) { + return str.replace(/[A-Z]/g, function($0) { return '-' + $0.toLowerCase() }); + } +}; + +/************************************** + * Process styles + **************************************/ +(function(){ + setTimeout(function(){ + $('link[rel="stylesheet"]').forEach(StyleFix.link); + }, 10); + + document.addEventListener('DOMContentLoaded', StyleFix.process, false); +})(); + +function $(expr, con) { + return [].slice.call((con || document).querySelectorAll(expr)); +} + +})(); + +/** + * PrefixFree + */ +(function(root){ + +if(!window.StyleFix || !window.getComputedStyle) { + return; +} + +// Private helper +function fix(what, before, after, replacement, css) { + what = self[what]; + + if(what.length) { + var regex = RegExp(before + '(' + what.join('|') + ')' + after, 'gi'); + + css = css.replace(regex, replacement); + } + + return css; +} + +var self = window.PrefixFree = { + prefixCSS: function(css, raw, element) { + var prefix = self.prefix; + + // Gradient angles hotfix + if(self.functions.indexOf('linear-gradient') > -1) { + // Gradients are supported with a prefix, convert angles to legacy + css = css.replace(/(\s|:|,)(repeating-)?linear-gradient\(\s*(-?\d*\.?\d*)deg/ig, function ($0, delim, repeating, deg) { + return delim + (repeating || '') + 'linear-gradient(' + (90-deg) + 'deg'; + }); + } + + css = fix('functions', '(\\s|:|,)', '\\s*\\(', '$1' + prefix + '$2(', css); + css = fix('keywords', '(\\s|:)', '(\\s|;|\\}|$)', '$1' + prefix + '$2$3', css); + css = fix('properties', '(^|\\{|\\s|;)', '\\s*:', '$1' + prefix + '$2:', css); + + // Prefix properties *inside* values (issue #8) + if (self.properties.length) { + var regex = RegExp('\\b(' + self.properties.join('|') + ')(?!:)', 'gi'); + + css = fix('valueProperties', '\\b', ':(.+?);', function($0) { + return $0.replace(regex, prefix + "$1") + }, css); + } + + if(raw) { + css = fix('selectors', '', '\\b', self.prefixSelector, css); + css = fix('atrules', '@', '\\b', '@' + prefix + '$1', css); + } + + // Fix double prefixing + css = css.replace(RegExp('-' + prefix, 'g'), '-'); + + // Prefix wildcard + css = css.replace(/-\*-(?=[a-z]+)/gi, self.prefix); + + return css; + }, + + property: function(property) { + return (self.properties.indexOf(property)? self.prefix : '') + property; + }, + + value: function(value, property) { + value = fix('functions', '(^|\\s|,)', '\\s*\\(', '$1' + self.prefix + '$2(', value); + value = fix('keywords', '(^|\\s)', '(\\s|$)', '$1' + self.prefix + '$2$3', value); + + // TODO properties inside values + + return value; + }, + + // Warning: Prefixes no matter what, even if the selector is supported prefix-less + prefixSelector: function(selector) { + return selector.replace(/^:{1,2}/, function($0) { return $0 + self.prefix }) + }, + + // Warning: Prefixes no matter what, even if the property is supported prefix-less + prefixProperty: function(property, camelCase) { + var prefixed = self.prefix + property; + + return camelCase? StyleFix.camelCase(prefixed) : prefixed; + } +}; + +/************************************** + * Properties + **************************************/ +(function() { + var prefixes = {}, + properties = [], + shorthands = {}, + style = getComputedStyle(document.documentElement, null), + dummy = document.createElement('div').style; + + // Why are we doing this instead of iterating over properties in a .style object? Cause Webkit won't iterate over those. + var iterate = function(property) { + if(property.charAt(0) === '-') { + properties.push(property); + + var parts = property.split('-'), + prefix = parts[1]; + + // Count prefix uses + prefixes[prefix] = ++prefixes[prefix] || 1; + + // This helps determining shorthands + while(parts.length > 3) { + parts.pop(); + + var shorthand = parts.join('-'); + + if(supported(shorthand) && properties.indexOf(shorthand) === -1) { + properties.push(shorthand); + } + } + } + }, + supported = function(property) { + return StyleFix.camelCase(property) in dummy; + } + + // Some browsers have numerical indices for the properties, some don't + if(style.length > 0) { + for(var i=0; i<style.length; i++) { + iterate(style[i]) + } + } + else { + for(var property in style) { + iterate(StyleFix.deCamelCase(property)); + } + } + + // Find most frequently used prefix + var highest = {uses:0}; + for(var prefix in prefixes) { + var uses = prefixes[prefix]; + + if(highest.uses < uses) { + highest = {prefix: prefix, uses: uses}; + } + } + + self.prefix = '-' + highest.prefix + '-'; + self.Prefix = StyleFix.camelCase(self.prefix); + + self.properties = []; + + // Get properties ONLY supported with a prefix + for(var i=0; i<properties.length; i++) { + var property = properties[i]; + + if(property.indexOf(self.prefix) === 0) { // we might have multiple prefixes, like Opera + var unprefixed = property.slice(self.prefix.length); + + if(!supported(unprefixed)) { + self.properties.push(unprefixed); + } + } + } + + // IE fix + if(self.Prefix == 'Ms' + && !('transform' in dummy) + && !('MsTransform' in dummy) + && ('msTransform' in dummy)) { + self.properties.push('transform', 'transform-origin'); + } + + self.properties.sort(); +})(); + +/************************************** + * Values + **************************************/ +(function() { +// Values that might need prefixing +var functions = { + 'linear-gradient': { + property: 'backgroundImage', + params: 'red, teal' + }, + 'calc': { + property: 'width', + params: '1px + 5%' + }, + 'element': { + property: 'backgroundImage', + params: '#foo' + }, + 'cross-fade': { + property: 'backgroundImage', + params: 'url(a.png), url(b.png), 50%' + } +}; + + +functions['repeating-linear-gradient'] = +functions['repeating-radial-gradient'] = +functions['radial-gradient'] = +functions['linear-gradient']; + +// Note: The properties assigned are just to *test* support. +// The keywords will be prefixed everywhere. +var keywords = { + 'initial': 'color', + 'zoom-in': 'cursor', + 'zoom-out': 'cursor', + 'box': 'display', + 'flexbox': 'display', + 'inline-flexbox': 'display', + 'flex': 'display', + 'inline-flex': 'display', + 'grid': 'display', + 'inline-grid': 'display', + 'min-content': 'width' +}; + +self.functions = []; +self.keywords = []; + +var style = document.createElement('div').style; + +function supported(value, property) { + style[property] = ''; + style[property] = value; + + return !!style[property]; +} + +for (var func in functions) { + var test = functions[func], + property = test.property, + value = func + '(' + test.params + ')'; + + if (!supported(value, property) + && supported(self.prefix + value, property)) { + // It's supported, but with a prefix + self.functions.push(func); + } +} + +for (var keyword in keywords) { + var property = keywords[keyword]; + + if (!supported(keyword, property) + && supported(self.prefix + keyword, property)) { + // It's supported, but with a prefix + self.keywords.push(keyword); + } +} + +})(); + +/************************************** + * Selectors and @-rules + **************************************/ +(function() { + +var +selectors = { + ':read-only': null, + ':read-write': null, + ':any-link': null, + '::selection': null +}, + +atrules = { + 'keyframes': 'name', + 'viewport': null, + 'document': 'regexp(".")' +}; + +self.selectors = []; +self.atrules = []; + +var style = root.appendChild(document.createElement('style')); + +function supported(selector) { + style.textContent = selector + '{}'; // Safari 4 has issues with style.innerHTML + + return !!style.sheet.cssRules.length; +} + +for(var selector in selectors) { + var test = selector + (selectors[selector]? '(' + selectors[selector] + ')' : ''); + + if(!supported(test) && supported(self.prefixSelector(test))) { + self.selectors.push(selector); + } +} + +for(var atrule in atrules) { + var test = atrule + ' ' + (atrules[atrule] || ''); + + if(!supported('@' + test) && supported('@' + self.prefix + test)) { + self.atrules.push(atrule); + } +} + +root.removeChild(style); + +})(); + +// Properties that accept properties as their value +self.valueProperties = [ + 'transition', + 'transition-property' +] + +// Add class for current prefix +root.className += ' ' + self.prefix; + +StyleFix.register(self.prefixCSS); + + +})(document.documentElement);
\ No newline at end of file diff --git a/theme/assets/less/style.less b/theme/assets/less/style.less new file mode 100644 index 0000000..b2f68aa --- /dev/null +++ b/theme/assets/less/style.less @@ -0,0 +1,38 @@ +html { + background: #faf9f4; +} +body { + background: transparent; + top: 0; + bottom:0; + left:0; + right:0; +} +.logo { + padding-top: 30px; + h1 { + font-size: 60px; + small { + font-size: 30px; + + } + } +} + +*, body, html, .article, h1, h2, h3, h4, h5, h6, p, div, span, strong, a, li, ul, ol, nav { + font-family: "Montserrat"; +} + +.httpretty-navbar { + .uk-navbar-brand{ + font-size: 28px; + padding-top: 4px; + small { + font-size:14px; + } + } +} + +a.uk-button-primary { + color:white; +}
\ No newline at end of file diff --git a/theme/assets/license.txt b/theme/assets/license.txt new file mode 100644 index 0000000..4a325c7 --- /dev/null +++ b/theme/assets/license.txt @@ -0,0 +1,9 @@ +<!-- +Copyright (c) 2013 by Gabriel Falcão (http://codepen.io/gabrielfalcao/pen/Esdub) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-->
\ No newline at end of file diff --git a/theme/content.html b/theme/content.html new file mode 100644 index 0000000..3426d53 --- /dev/null +++ b/theme/content.html @@ -0,0 +1,80 @@ + {% macro render_menu_item(header, relative_path) -%} + {% if header['level'] < 3 and header['level'] > 1 %} + <li><a href="{{ relative_path.replace(".md", ".html") }}{{ header.anchor }}">{{ header.text|safe }}</a></li> + {% endif %} + {% if 'child' in header %} + {% for child in header['child'] %} + {{ render_menu_item(child, relative_path) }} + {% endfor %} + {% endif %} + {%- endmacro %} + + <div class="uk-container uk-container-center" style="padding: 120px 0;"> + <div class="uk-grid" data-uk-grid-margin=""> + <div class="uk-width-medium-1-4 uk-hidden-small"> + <div style="clear:both; padding-bottom: 50px;"> + <img src="{{ project.logo_url }}" width="240" title="{{ project.name }}" alt="{{ project.name }}" /> + <h2>{{ project.name }} v{{ project.version }}</h2> + <p> + <iframe src="http://instanc.es/bin/btn/watchers-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/forks-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> + <iframe src="http://instanc.es/bin/btn/follow-gabrielfalcao-HTTPretty-small.html" allowtransparency="true" frameborder="0" scrolling="0" width="152px" height="30px"></iframe> +</p> + <a class="uk-navbar-brand" href="#">Table of Contents</a> + </div> + + <ul style="clear:both;" class="uk-nav"> + {% for page in master_index %} + {% if page.relative_path.endswith("intro.md") %} + <li class="uk-nav-header">{{ page.indexes[0].text }}</li> + {% for item in page.indexes %} + {{ render_menu_item(item, page.relative_path) }} + {% endfor %} + {% endif %} + {% endfor %} + {% for page in master_index %} + {% if page.relative_path.endswith("docs.md") %} + <li class="uk-nav-header">{{ page.indexes[0].text }}</li> + {% for item in page.indexes %} + {{ render_menu_item(item, page.relative_path) }} + {% endfor %} + {% endif %} + {% endfor %} + {% for page in master_index %} + {% if page.relative_path.endswith("about.md") %} + <li class="uk-nav-header">{{ page.indexes[0].text }}</li> + {% for item in page.indexes %} + {{ render_menu_item(item, page.relative_path) }} + {% endfor %} + {% endif %} + {% endfor %} + {% for page in master_index %} + {% if page.relative_path.endswith("contributing.md") %} + <li class="uk-nav-header">{{ page.indexes[0].text }}</li> + {% for item in page.indexes %} + {{ render_menu_item(item, page.relative_path) }} + {% endfor %} + {% endif %} + {% endfor %} + {% for page in master_index %} + {% if page.relative_path.endswith("NEWS.md") %} + <li class="uk-nav-header">{{ page.indexes[0].text }}</li> + {% for item in page.indexes %} + {{ render_menu_item(item, page.relative_path) }} + {% endfor %} + {% endif %} + {% endfor %} + </ul> + </div> + <div class="uk-width-medium-3-4"> + <article class="uk-article"> + <h1 class="uk-article-title">{{ project.name }}</h1> + <p class="uk-article-lead">{{ project.description }}</p> + <div class="uk-width-medium-1-1"> + {{ documentation|safe }} + </div> + </div> + </article> + </div> + </div> + </div> diff --git a/theme/index.html b/theme/index.html new file mode 100644 index 0000000..dbe2660 --- /dev/null +++ b/theme/index.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>{{ project.name }} by {{ project.github_maintainer }}</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <!-- Loading Bootstrap --> + + <link rel='stylesheet prefetch' href='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.css'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Montserrat:400,700'> + <link rel='stylesheet prefetch' href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700,900,200italic,300italic,400italic,600italic,700italic,900italic'> + <link rel='stylesheet prefetch' href='//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css'> + <link href="{{ static_file('css/github.css') }}" rel="stylesheet"> + <link href="{{ static_file('css/style.css') }}" rel="stylesheet"> + <script src="{{ static_file('js/prefixfree.min.js') }}"></script> + </head> + <body> + {% include "./content.html" %} + <script type="text/javascript"> + var _gaq = _gaq || []; + _gaq.push(['_setAccount', '{{ project.google_analytics_code }}']); + _gaq.push(['_trackPageview']); + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); + </script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/jquery-2.0.3.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/angular.min.js'></script> + <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/uikit.min.js'></script> + </body> +</html>
\ No newline at end of file diff --git a/theme/logo.html b/theme/logo.html new file mode 100644 index 0000000..f5106ba --- /dev/null +++ b/theme/logo.html @@ -0,0 +1,33 @@ +<div class="uk-container uk-container-center uk-text-center"> + <div class="logo"> + <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/18885/httpretty-logo_1.svg" width="300" title="HTTPretty" alt="HTTPretty" /> + <h1>{{ project.name }} <small>{{ project.version }}</small></h1> + </div> + <p class="uk-text-large">{{ project.tagline }}</p> + <p><a class="uk-button uk-button-primary" href="./docs.html">Check the documentation</a></p> + <ul class="tm-subnav uk-subnav"> + <li> + <a href="https://twitter.com/{{ project.twitter }}" class="twitter-follow-button" data-show-count="false" data-dnt="true">Follow @{{ project.twitter }}</a> + <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script></li> + <li><a href="https://twitter.com/intent/tweet?button_hashtag=HTTPretty&text={{ project.twitter_message }}" class="twitter-hashtag-button" data-related="gabrielfalcao" data-url="{{ project.documentation_url }}" data-dnt="true">Tweet #{{ project.name }}</a> + <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script></li> + </ul> + <ul class="tm-subnav uk-subnav"> + <li> + <iframe src="http://ghbtns.com/github-btn.html?user={{ project.github_maintainer }}&type=follow&count=true" + allowtransparency="true" + frameborder="0" + scrolling="0" + width="165" + height="20"></iframe> + </li> + <li> + <iframe src="http://ghbtns.com/github-btn.html?user={{ project.github_maintainer }}&repo=github-buttons&type=watch&count=true" + allowtransparency="true" + frameborder="0" + scrolling="0" + width="110" + height="20"></iframe> + </li> + </ul> +</div> diff --git a/theme/markment.yml b/theme/markment.yml new file mode 100644 index 0000000..6c7b21c --- /dev/null +++ b/theme/markment.yml @@ -0,0 +1,8 @@ +index_template: index.html +assets_path: assets +author: + name: Gabriel Falcao + username: gabrielfalcao + website: http://falcao.it + +github_url: https://github.com/gabrielfalcao/markment @@ -0,0 +1,9 @@ +[tox] +envlist = py26, py27, py33 + +[testenv] +deps = -r{toxinidir}/requirements.pip +commands = + {envpython} setup.py test + nosetests tests/unit + nosetests tests/functional |
