diff --git a/fixtures/main_error_response.yaml b/fixtures/main_error_response.yaml new file mode 100644 index 0000000..30d6fd7 --- /dev/null +++ b/fixtures/main_error_response.yaml @@ -0,0 +1,30 @@ +interactions: +- request: + body: null + headers: + Content-Length: + - '0' + method: '' + uri: https://jsonplaceholder.typicode.com/ + response: + body: + string: "\r\n400 Bad Request\r\n\r\n\ +

400 Bad Request

\r\n
cloudflare
\r\ + \n\r\n\r\n" + headers: + CF-RAY: + - '-' + Connection: + - close + Content-Length: + - '155' + Content-Type: + - text/html + Date: + - Mon, 16 Dec 2019 22:11:39 GMT + Server: + - cloudflare + status: + code: 400 + message: Bad Request +version: 1 diff --git a/fixtures/main_input_body.yaml b/fixtures/main_input_body.yaml new file mode 100644 index 0000000..0ea82b1 --- /dev/null +++ b/fixtures/main_input_body.yaml @@ -0,0 +1,59 @@ +interactions: +- request: + body: id=42&title=test + headers: + Content-Length: + - '16' + Content-Type: + - application/x-www-form-urlencoded + method: POST + uri: https://jsonplaceholder.typicode.com/posts + response: + body: + string: "{\n \"id\": 101,\n \"title\": \"test\"\n}" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Expose-Headers: + - Location + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 546ab6213f81f11a-IAD + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Length: + - '34' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 17 Dec 2019 17:45:33 GMT + Etag: + - W/"22-i04alCk7PdGrJ2UKCUwBOO0LB3w" + Expect-CT: + - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Expires: + - '-1' + Location: + - http://jsonplaceholder.typicode.com/posts/101 + Pragma: + - no-cache + Server: + - cloudflare + Set-Cookie: + - __cfduid=d1fa880178dc2db917f26b8ce4e0d56e41576604733; expires=Thu, 16-Jan-20 + 17:45:33 GMT; path=/; domain=.typicode.com; HttpOnly; SameSite=Lax + Vary: + - Origin, X-HTTP-Method-Override, Accept-Encoding + Via: + - 1.1 vegur + X-Content-Type-Options: + - nosniff + X-Powered-By: + - Express + status: + code: 201 + message: Created +version: 1 diff --git a/fixtures/main_input_headers.yaml b/fixtures/main_input_headers.yaml new file mode 100644 index 0000000..606bfa7 --- /dev/null +++ b/fixtures/main_input_headers.yaml @@ -0,0 +1,94 @@ +interactions: +- request: + body: null + headers: + X-Test-Header: + - th + method: GET + uri: https://jsonplaceholder.typicode.com/posts?userId=1 + response: + body: + string: "[\n {\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"sunt aut\ + \ facere repellat provident occaecati excepturi optio reprehenderit\",\n \ + \ \"body\": \"quia et suscipit\\nsuscipit recusandae consequuntur expedita\ + \ et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem\ + \ sunt rem eveniet architecto\"\n },\n {\n \"userId\": 1,\n \"id\"\ + : 2,\n \"title\": \"qui est esse\",\n \"body\": \"est rerum tempore\ + \ vitae\\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\\nfugiat\ + \ blanditiis voluptate porro vel nihil molestiae ut reiciendis\\nqui aperiam\ + \ non debitis possimus qui neque nisi nulla\"\n },\n {\n \"userId\":\ + \ 1,\n \"id\": 3,\n \"title\": \"ea molestias quasi exercitationem repellat\ + \ qui ipsa sit aut\",\n \"body\": \"et iusto sed quo iure\\nvoluptatem\ + \ occaecati omnis eligendi aut ad\\nvoluptatem doloribus vel accusantium quis\ + \ pariatur\\nmolestiae porro eius odio et labore et velit aut\"\n },\n {\n\ + \ \"userId\": 1,\n \"id\": 4,\n \"title\": \"eum et est occaecati\"\ + ,\n \"body\": \"ullam et saepe reiciendis voluptatem adipisci\\nsit amet\ + \ autem assumenda provident rerum culpa\\nquis hic commodi nesciunt rem tenetur\ + \ doloremque ipsam iure\\nquis sunt voluptatem rerum illo velit\"\n },\n\ + \ {\n \"userId\": 1,\n \"id\": 5,\n \"title\": \"nesciunt quas odio\"\ + ,\n \"body\": \"repudiandae veniam quaerat sunt sed\\nalias aut fugiat\ + \ sit autem sed est\\nvoluptatem omnis possimus esse voluptatibus quis\\nest\ + \ aut tenetur dolor neque\"\n },\n {\n \"userId\": 1,\n \"id\": 6,\n\ + \ \"title\": \"dolorem eum magni eos aperiam quia\",\n \"body\": \"\ + ut aspernatur corporis harum nihil quis provident sequi\\nmollitia nobis aliquid\ + \ molestiae\\nperspiciatis et ea nemo ab reprehenderit accusantium quas\\\ + nvoluptate dolores velit et doloremque molestiae\"\n },\n {\n \"userId\"\ + : 1,\n \"id\": 7,\n \"title\": \"magnam facilis autem\",\n \"body\"\ + : \"dolore placeat quibusdam ea quo vitae\\nmagni quis enim qui quis quo nemo\ + \ aut saepe\\nquidem repellat excepturi ut quia\\nsunt ut sequi eos ea sed\ + \ quas\"\n },\n {\n \"userId\": 1,\n \"id\": 8,\n \"title\": \"\ + dolorem dolore est ipsam\",\n \"body\": \"dignissimos aperiam dolorem qui\ + \ eum\\nfacilis quibusdam animi sint suscipit qui sint possimus cum\\nquaerat\ + \ magni maiores excepturi\\nipsam ut commodi dolor voluptatum modi aut vitae\"\ + \n },\n {\n \"userId\": 1,\n \"id\": 9,\n \"title\": \"nesciunt\ + \ iure omnis dolorem tempora et accusantium\",\n \"body\": \"consectetur\ + \ animi nesciunt iure dolore\\nenim quia ad\\nveniam autem ut quam aut nobis\\\ + net est aut quod aut provident voluptas autem voluptas\"\n },\n {\n \"\ + userId\": 1,\n \"id\": 10,\n \"title\": \"optio molestias id quia eum\"\ + ,\n \"body\": \"quo et expedita modi cum officia vel magni\\ndoloribus\ + \ qui repudiandae\\nvero nisi sit\\nquos veniam quod sed accusamus veritatis\ + \ error\"\n }\n]" + headers: + Access-Control-Allow-Credentials: + - 'true' + Age: + - '1789' + CF-Cache-Status: + - HIT + CF-RAY: + - 54722d954de5e0ea-IAD + Cache-Control: + - max-age=14400 + Connection: + - keep-alive + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 18 Dec 2019 15:30:26 GMT + Etag: + - W/"aa6-j2NSH739l9uq40OywFMn7Y0C/iY" + Expect-CT: + - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Expires: + - '-1' + Pragma: + - no-cache + Server: + - cloudflare + Set-Cookie: + - __cfduid=d21e159ad4987bc5ba9d781aed2f9db5c1576683026; expires=Fri, 17-Jan-20 + 15:30:26 GMT; path=/; domain=.typicode.com; HttpOnly; SameSite=Lax + Transfer-Encoding: + - chunked + Vary: + - Origin, Accept-Encoding + Via: + - 1.1 vegur + X-Content-Type-Options: + - nosniff + X-Powered-By: + - Express + status: + code: 200 + message: OK +version: 1 diff --git a/fixtures/main_json_response.yaml b/fixtures/main_json_response.yaml new file mode 100644 index 0000000..782bee3 --- /dev/null +++ b/fixtures/main_json_response.yaml @@ -0,0 +1,92 @@ +interactions: +- request: + body: null + headers: {} + method: GET + uri: https://jsonplaceholder.typicode.com/posts?userId=1 + response: + body: + string: "[\n {\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"sunt aut\ + \ facere repellat provident occaecati excepturi optio reprehenderit\",\n \ + \ \"body\": \"quia et suscipit\\nsuscipit recusandae consequuntur expedita\ + \ et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem\ + \ sunt rem eveniet architecto\"\n },\n {\n \"userId\": 1,\n \"id\"\ + : 2,\n \"title\": \"qui est esse\",\n \"body\": \"est rerum tempore\ + \ vitae\\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\\nfugiat\ + \ blanditiis voluptate porro vel nihil molestiae ut reiciendis\\nqui aperiam\ + \ non debitis possimus qui neque nisi nulla\"\n },\n {\n \"userId\":\ + \ 1,\n \"id\": 3,\n \"title\": \"ea molestias quasi exercitationem repellat\ + \ qui ipsa sit aut\",\n \"body\": \"et iusto sed quo iure\\nvoluptatem\ + \ occaecati omnis eligendi aut ad\\nvoluptatem doloribus vel accusantium quis\ + \ pariatur\\nmolestiae porro eius odio et labore et velit aut\"\n },\n {\n\ + \ \"userId\": 1,\n \"id\": 4,\n \"title\": \"eum et est occaecati\"\ + ,\n \"body\": \"ullam et saepe reiciendis voluptatem adipisci\\nsit amet\ + \ autem assumenda provident rerum culpa\\nquis hic commodi nesciunt rem tenetur\ + \ doloremque ipsam iure\\nquis sunt voluptatem rerum illo velit\"\n },\n\ + \ {\n \"userId\": 1,\n \"id\": 5,\n \"title\": \"nesciunt quas odio\"\ + ,\n \"body\": \"repudiandae veniam quaerat sunt sed\\nalias aut fugiat\ + \ sit autem sed est\\nvoluptatem omnis possimus esse voluptatibus quis\\nest\ + \ aut tenetur dolor neque\"\n },\n {\n \"userId\": 1,\n \"id\": 6,\n\ + \ \"title\": \"dolorem eum magni eos aperiam quia\",\n \"body\": \"\ + ut aspernatur corporis harum nihil quis provident sequi\\nmollitia nobis aliquid\ + \ molestiae\\nperspiciatis et ea nemo ab reprehenderit accusantium quas\\\ + nvoluptate dolores velit et doloremque molestiae\"\n },\n {\n \"userId\"\ + : 1,\n \"id\": 7,\n \"title\": \"magnam facilis autem\",\n \"body\"\ + : \"dolore placeat quibusdam ea quo vitae\\nmagni quis enim qui quis quo nemo\ + \ aut saepe\\nquidem repellat excepturi ut quia\\nsunt ut sequi eos ea sed\ + \ quas\"\n },\n {\n \"userId\": 1,\n \"id\": 8,\n \"title\": \"\ + dolorem dolore est ipsam\",\n \"body\": \"dignissimos aperiam dolorem qui\ + \ eum\\nfacilis quibusdam animi sint suscipit qui sint possimus cum\\nquaerat\ + \ magni maiores excepturi\\nipsam ut commodi dolor voluptatum modi aut vitae\"\ + \n },\n {\n \"userId\": 1,\n \"id\": 9,\n \"title\": \"nesciunt\ + \ iure omnis dolorem tempora et accusantium\",\n \"body\": \"consectetur\ + \ animi nesciunt iure dolore\\nenim quia ad\\nveniam autem ut quam aut nobis\\\ + net est aut quod aut provident voluptas autem voluptas\"\n },\n {\n \"\ + userId\": 1,\n \"id\": 10,\n \"title\": \"optio molestias id quia eum\"\ + ,\n \"body\": \"quo et expedita modi cum officia vel magni\\ndoloribus\ + \ qui repudiandae\\nvero nisi sit\\nquos veniam quod sed accusamus veritatis\ + \ error\"\n }\n]" + headers: + Access-Control-Allow-Credentials: + - 'true' + Age: + - '4646' + CF-Cache-Status: + - HIT + CF-RAY: + - 54640259fa6fe0d2-IAD + Cache-Control: + - max-age=14400 + Connection: + - keep-alive + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 16 Dec 2019 22:14:15 GMT + Etag: + - W/"aa6-j2NSH739l9uq40OywFMn7Y0C/iY" + Expect-CT: + - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Expires: + - '-1' + Pragma: + - no-cache + Server: + - cloudflare + Set-Cookie: + - __cfduid=d8ac1a396d31b9e9c1f816924e5bf186d1576534455; expires=Wed, 15-Jan-20 + 22:14:15 GMT; path=/; domain=.typicode.com; HttpOnly + Transfer-Encoding: + - chunked + Vary: + - Origin, Accept-Encoding + Via: + - 1.1 vegur + X-Content-Type-Options: + - nosniff + X-Powered-By: + - Express + status: + code: 200 + message: OK +version: 1 diff --git a/fixtures/main_non_json_response.yaml b/fixtures/main_non_json_response.yaml new file mode 100644 index 0000000..e5e02a9 --- /dev/null +++ b/fixtures/main_non_json_response.yaml @@ -0,0 +1,145 @@ +interactions: +- request: + body: null + headers: {} + method: GET + uri: https://jsonplaceholder.typicode.com/ + response: + body: + string: "\n\n\n\n\n\n\n\n\n\n\nJSONPlaceholder - Fake online REST API for developers\n\ + \n\n
\n\n\ + \n Announcement: You can now support\n\ + JSONPlaceholder on GitHub Sponsors!\n\n
\n
\n\n
\n
\n

\nJSONPlaceholder\n\ +

\n

\nFake Online REST API for Testing and Prototyping\n\ +
Serving ~350M requests per month\n
Powered by\nJSON Server\n+\nLowDB\n

\n\n\n\n\n\n
\n
\n\n
\n

Gold\ + \ Sponsors

\n

\n\n\n\n

\n

\nYour Company Logo Here\n

\n
\n\n
\n
\n\ + \n

Intro

\n

\nJSONPlaceholder is a free online REST API that you\ + \ can use whenever you need some fake data.\n
It's great for tutorials,\ + \ testing new libraries, sharing code examples, ...\n

\n\n

Example

\n\ +

\nRun this code in a console or from any site:\n

\n
fetch('https://jsonplaceholder.typicode.com/todos/1')\n\
+        \  .then(response => response.json())\n  .then(json => console.log(json))\n\
+        
\n

\n\n

\n
\nCongrats\ + \ you've made your first call to JSONPlaceholder! \U0001F603 \U0001F389\n\ +

\nTip: you can use\n\nhttp://\n or\n\nhttps://\n\ + when making requests to JSONPlaceholder.\n

\n
\n\n\n

Resources

\n

\nJSONPlaceholder comes with a\ + \ set of 6 common resources:\n

\n\n\n\n\n\n\n\ + \n\n\ + \n\n\n\n\ + \n\n\n\n\ + \n\n\n\n\ + \n\n\n\n\ + \n
\n\ + \ /posts\n100 posts
\n/comments\n500 comments
\n/albums\n100 albums
\n/photos\n5000 photos
\n/todos\n200 todos
\n/users\n10 users
\n

\nNote: resources have relations. For\ + \ example:\nposts have many\ncomments,\nalbums have many\n\ + photos, ... see below for routes examples.\n

\n\n

Routes

\n\ +

\nAll HTTP methods are supported.\n

\n\n\n\n\ + \n\n\n\n\n\n\n\n\n\n\n\ + \n\n\n\n\n\n\n\n\n\n\n\n\n\ + \n\n\n\n\n\n\ + \n\n\n\n\n
GET\n/posts\n
GET\n\ + /posts/1\n
GET\n\ + /posts/1/comments\n
GET\n/comments?postId=1\n\ +
GET\n/posts?userId=1\n\ +
POST/posts
PUT/posts/1
PATCH/posts/1
DELETE/posts/1
\n

\nNote:\ + \ you can view detailed examples\nhere.\n

\n\ + \n

Use your own data

\n\n

\nWith My JSON Server online service and a simple GitHub repo, you can have\ + \ your own online fake REST server in seconds.\n

\n
\n
\n
\n\ + \n\n\n\n\n\ + \n\n\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Age: + - '5668' + CF-Cache-Status: + - HIT + CF-RAY: + - 5463fe6ccb0ecf04-IAD + Cache-Control: + - public, max-age=14400 + Connection: + - keep-alive + Content-Type: + - text/html; charset=UTF-8 + Date: + - Mon, 16 Dec 2019 22:11:34 GMT + Expect-CT: + - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Last-Modified: + - Mon, 05 Aug 2019 03:07:14 GMT + Server: + - cloudflare + Set-Cookie: + - __cfduid=d13b55a7f6e786f74d5f9f1f084a183a31576534294; expires=Wed, 15-Jan-20 + 22:11:34 GMT; path=/; domain=.typicode.com; HttpOnly + Transfer-Encoding: + - chunked + Vary: + - Origin, Accept-Encoding + Via: + - 1.1 vegur + X-Powered-By: + - Express + status: + code: 200 + message: OK +version: 1 diff --git a/fixtures/proxy_bad_request.yaml b/fixtures/proxy_bad_request.yaml new file mode 100644 index 0000000..9d71f3d --- /dev/null +++ b/fixtures/proxy_bad_request.yaml @@ -0,0 +1,20 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + method: GET + uri: http://localhost:8000/bad + response: + body: + string: '' + headers: + Date: + - Mon, 16 Dec 2019 23:44:53 GMT + Server: + - BaseHTTP/0.6 Python/3.7.3 + status: + code: 400 + message: Bad Request +version: 1 diff --git a/fixtures/proxy_callbacks.yaml b/fixtures/proxy_callbacks.yaml new file mode 100644 index 0000000..7224c8c --- /dev/null +++ b/fixtures/proxy_callbacks.yaml @@ -0,0 +1,145 @@ +interactions: +- request: + body: null + headers: {} + method: GET + uri: https://jsonplaceholder.typicode.com/ + response: + body: + string: "\n\n\n\n\n\n\n\n\n\n\nJSONPlaceholder - Fake online REST API for developers\n\ + \n\n
\n\n\ + \n Announcement: You can now support\n\ + JSONPlaceholder on GitHub Sponsors!\n\n
\n
\n\n
\n
\n

\nJSONPlaceholder\n\ +

\n

\nFake Online REST API for Testing and Prototyping\n\ +
Serving ~350M requests per month\n
Powered by\nJSON Server\n+\nLowDB\n

\n\n\n\n\n\n
\n
\n\n
\n

Gold\ + \ Sponsors

\n

\n\n\n\n

\n

\nYour Company Logo Here\n

\n
\n\n
\n
\n\ + \n

Intro

\n

\nJSONPlaceholder is a free online REST API that you\ + \ can use whenever you need some fake data.\n
It's great for tutorials,\ + \ testing new libraries, sharing code examples, ...\n

\n\n

Example

\n\ +

\nRun this code in a console or from any site:\n

\n
fetch('https://jsonplaceholder.typicode.com/todos/1')\n\
+        \  .then(response => response.json())\n  .then(json => console.log(json))\n\
+        
\n

\n\n

\n
\nCongrats\ + \ you've made your first call to JSONPlaceholder! \U0001F603 \U0001F389\n\ +

\nTip: you can use\n\nhttp://\n or\n\nhttps://\n\ + when making requests to JSONPlaceholder.\n

\n
\n\n\n

Resources

\n

\nJSONPlaceholder comes with a\ + \ set of 6 common resources:\n

\n\n\n\n\n\n\n\ + \n\n\ + \n\n\n\n\ + \n\n\n\n\ + \n\n\n\n\ + \n\n\n\n\ + \n
\n\ + \ /posts\n100 posts
\n/comments\n500 comments
\n/albums\n100 albums
\n/photos\n5000 photos
\n/todos\n200 todos
\n/users\n10 users
\n

\nNote: resources have relations. For\ + \ example:\nposts have many\ncomments,\nalbums have many\n\ + photos, ... see below for routes examples.\n

\n\n

Routes

\n\ +

\nAll HTTP methods are supported.\n

\n\n\n\n\ + \n\n\n\n\n\n\n\n\n\n\n\ + \n\n\n\n\n\n\n\n\n\n\n\n\n\ + \n\n\n\n\n\n\ + \n\n\n\n\n
GET\n/posts\n
GET\n\ + /posts/1\n
GET\n\ + /posts/1/comments\n
GET\n/comments?postId=1\n\ +
GET\n/posts?userId=1\n\ +
POST/posts
PUT/posts/1
PATCH/posts/1
DELETE/posts/1
\n

\nNote:\ + \ you can view detailed examples\nhere.\n

\n\ + \n

Use your own data

\n\n

\nWith My JSON Server online service and a simple GitHub repo, you can have\ + \ your own online fake REST server in seconds.\n

\n
\n
\n
\n\ + \n\n\n\n\n\ + \n\n\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Age: + - '4432' + CF-Cache-Status: + - HIT + CF-RAY: + - 5469de47682ecf00-IAD + Cache-Control: + - public, max-age=14400 + Connection: + - keep-alive + Content-Type: + - text/html; charset=UTF-8 + Date: + - Tue, 17 Dec 2019 15:18:12 GMT + Expect-CT: + - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Last-Modified: + - Mon, 05 Aug 2019 03:07:14 GMT + Server: + - cloudflare + Set-Cookie: + - __cfduid=dc33ebab29f22c648b958d03fed5596b51576595892; expires=Thu, 16-Jan-20 + 15:18:12 GMT; path=/; domain=.typicode.com; HttpOnly; SameSite=Lax + Transfer-Encoding: + - chunked + Vary: + - Origin, Accept-Encoding + Via: + - 1.1 vegur + X-Powered-By: + - Express + status: + code: 200 + message: OK +version: 1 diff --git a/fixtures/proxy_cannot_connect.yaml b/fixtures/proxy_cannot_connect.yaml new file mode 100644 index 0000000..9788ab4 --- /dev/null +++ b/fixtures/proxy_cannot_connect.yaml @@ -0,0 +1,22 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + method: GET + uri: http://localhost:8000/ + response: + body: + string: 'hello + + ' + headers: + Date: + - Tue, 17 Dec 2019 00:06:22 GMT + Server: + - BaseHTTP/0.6 Python/3.7.3 + status: + code: 200 + message: OK +version: 1 diff --git a/fixtures/proxy_internal_error.yaml b/fixtures/proxy_internal_error.yaml new file mode 100644 index 0000000..7295de6 --- /dev/null +++ b/fixtures/proxy_internal_error.yaml @@ -0,0 +1,22 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + method: GET + uri: http://localhost:8000/ + response: + body: + string: 'hello + + ' + headers: + Date: + - Tue, 17 Dec 2019 00:00:13 GMT + Server: + - BaseHTTP/0.6 Python/3.7.3 + status: + code: 200 + message: OK +version: 1 diff --git a/fixtures/proxy_internal_server_error.yaml b/fixtures/proxy_internal_server_error.yaml new file mode 100644 index 0000000..603e624 --- /dev/null +++ b/fixtures/proxy_internal_server_error.yaml @@ -0,0 +1,20 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + method: GET + uri: http://localhost:8000/crash + response: + body: + string: '' + headers: + Date: + - Mon, 16 Dec 2019 23:58:10 GMT + Server: + - BaseHTTP/0.6 Python/3.7.3 + status: + code: 500 + message: Internal Server Error +version: 1 diff --git a/fixtures/proxy_unofficial_status.yaml b/fixtures/proxy_unofficial_status.yaml new file mode 100644 index 0000000..fa62ef9 --- /dev/null +++ b/fixtures/proxy_unofficial_status.yaml @@ -0,0 +1,20 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + method: GET + uri: http://localhost:8000/teapot + response: + body: + string: '' + headers: + Date: + - Mon, 16 Dec 2019 23:59:52 GMT + Server: + - BaseHTTP/0.6 Python/3.7.3 + status: + code: 418 + message: '' +version: 1 diff --git a/securedrop_proxy/entrypoint.py b/securedrop_proxy/entrypoint.py index 70f21a3..fb0a8c1 100755 --- a/securedrop_proxy/entrypoint.py +++ b/securedrop_proxy/entrypoint.py @@ -6,6 +6,8 @@ # called with exactly one argument: the path to its config file. See # the README for configuration options. +import http +import json import logging import os import sys @@ -30,60 +32,55 @@ def start(): ''' try: configure_logging() - except Exception as e: - print(e) - return - - logging.debug('Starting SecureDrop Proxy {}'.format(version)) - # a fresh, new proxy object - p = proxy.Proxy() + logging.debug('Starting SecureDrop Proxy {}'.format(version)) - # set up an error handler early, so we can use it during - # configuration, etc - p.on_done = callbacks.err_on_done + # a fresh, new proxy object + p = proxy.Proxy() - # path to config file must be at argv[1] - if len(sys.argv) != 2: - p.simple_error( - 500, "sd-proxy script not called with path to configuration file" - ) - p.on_done(p.res) + # set up an error handler early, so we can use it during + # configuration, etc + original_on_done = p.on_done + p.on_done = callbacks.err_on_done - # read config. `read_conf` will call `p.on_done` if there is a config - # problem, and will return a Conf object on success. - conf_path = sys.argv[1] - p.conf = config.read_conf(conf_path, p) + # path to config file must be at argv[1] + if len(sys.argv) != 2: + raise ValueError("sd-proxy script not called with path to configuration file") - # read user request from STDIN - incoming = [] - for line in sys.stdin: - incoming.append(line) - incoming = "\n".join(incoming) + # read config. `read_conf` will call `p.on_done` if there is a config + # problem, and will return a Conf object on success. + conf_path = sys.argv[1] + p.conf = config.read_conf(conf_path, p) - main.__main__(incoming, p) + # read user request from STDIN + incoming = [] + for line in sys.stdin: + incoming.append(line) + incoming = "\n".join(incoming) - -def excepthook(*exc_args): - ''' - This function is called in the event of a catastrophic failure. - Log exception and exit cleanly. - ''' - logging.error('Unrecoverable error', exc_info=(exc_args)) - sys.__excepthook__(*exc_args) - print('') # force terminal prompt on to a new line - sys.exit(1) + p.on_done = original_on_done + main.__main__(incoming, p) + except Exception as e: + response = { + "status": http.HTTPStatus.INTERNAL_SERVER_ERROR, + "body": json.dumps({ + "error": str(e), + }) + } + print(json.dumps(response)) + sys.exit(1) def configure_logging() -> None: ''' All logging related settings are set up by this function. ''' - log_folder = os.path.join(DEFAULT_HOME, 'logs') + home = os.getenv("SECUREDROP_HOME", DEFAULT_HOME) + log_folder = os.path.join(home, 'logs') if not os.path.exists(log_folder): os.makedirs(log_folder) - log_file = os.path.join(DEFAULT_HOME, 'logs', 'proxy.log') + log_file = os.path.join(home, 'logs', 'proxy.log') # set logging format log_fmt = ('%(asctime)s - %(name)s:%(lineno)d(%(funcName)s) %(levelname)s: %(message)s') @@ -98,6 +95,3 @@ def configure_logging() -> None: log = logging.getLogger() log.setLevel(LOGLEVEL) log.addHandler(handler) - - # override excepthook to capture a log of catastrophic failures. - sys.excepthook = excepthook diff --git a/securedrop_proxy/main.py b/securedrop_proxy/main.py index 6fadafa..e67f158 100644 --- a/securedrop_proxy/main.py +++ b/securedrop_proxy/main.py @@ -21,6 +21,7 @@ def __main__(incoming, p): logging.error(e) p.simple_error(400, 'Invalid JSON in request') p.on_done(p.res) + return req = proxy.Req() try: @@ -38,6 +39,6 @@ def __main__(incoming, p): req.body = client_req['body'] p.req = req - p.on_save = callbacks.on_save - p.on_done = callbacks.on_done + if not p.on_save: + p.on_save = callbacks.on_save p.proxy() diff --git a/securedrop_proxy/proxy.py b/securedrop_proxy/proxy.py index cc30b32..733d2ed 100644 --- a/securedrop_proxy/proxy.py +++ b/securedrop_proxy/proxy.py @@ -1,4 +1,5 @@ import furl +import http import json import logging import requests @@ -6,6 +7,7 @@ import werkzeug import securedrop_proxy.version as version +from securedrop_proxy import callbacks logger = logging.getLogger(__name__) @@ -28,22 +30,21 @@ def __init__(self, status): class Proxy: - @staticmethod - def _on_done(res): - print(json.dumps(res.__dict__)) - - def __init__(self, conf=None, req=Req(), on_save=None, on_done=None): + def __init__(self, conf=None, req=Req(), on_save=None, on_done=None, timeout: float = None): self.conf = conf self.req = req self.res = None self.on_save = on_save - if on_done is not None: + if on_done: self.on_done = on_done - else: - self.on_done = self._on_done + + self.timeout = float(timeout) if timeout else 10 self._prepared_request = None + def on_done(self, res): # type: ignore + callbacks.on_done(res) + @staticmethod def valid_path(path): u = furl.furl(path) @@ -75,7 +76,7 @@ def prep_request(self): try: url = furl.furl("{}://{}:{}/{}".format(scheme, host, port, path)) except Exception as e: - logging.error(e) + logger.error(e) self.simple_error(500, "Proxy error while generating URL to request") raise ValueError("Error generating URL from provided values") @@ -118,7 +119,7 @@ def handle_non_json_response(self): self.res = res def handle_response(self): - logging.debug('Handling response') + logger.debug("Handling response") ctype = werkzeug.http.parse_options_header(self._presp.headers["content-type"]) @@ -134,24 +135,46 @@ def handle_response(self): def proxy(self): try: - if self.on_save is None: - self.simple_error(400, "Request callback is not set.") - raise ValueError("Request callback is not set.") + if not self.on_save: + self.simple_error( + http.HTTPStatus.BAD_REQUEST, "Request on_save callback is not set." + ) + raise ValueError("Request on_save callback is not set.") self.prep_request() - logging.debug('Sending request') + logger.debug("Sending request") s = requests.Session() - self._presp = s.send(self._prepared_request) + self._presp = s.send(self._prepared_request, timeout=self.timeout) + self._presp.raise_for_status() self.handle_response() - except ValueError as e: - logging.error(e) + logger.error(e) # effectively a 4xx error # we have set self.response to indicate an error pass - - # catch server errors here, handle maybe-differently from - # ValueErrors... - + except requests.exceptions.Timeout as e: + # Timeout covers both ConnectTimeout and ReadTimeout + logger.error(e) + self.simple_error(http.HTTPStatus.GATEWAY_TIMEOUT, "request timed out") + except ( + requests.exceptions.ConnectionError, # covers ProxyError, SSLError + requests.exceptions.TooManyRedirects, + ) as e: + logger.error(e) + self.simple_error(http.HTTPStatus.BAD_GATEWAY, "could not connect to server") + except requests.exceptions.HTTPError as e: + logger.error(e) + try: + self.simple_error( + e.response.status_code, + http.HTTPStatus(e.response.status_code).phrase.lower() + ) + except ValueError: + # Return a generic error message when the response + # status code is not found in http.HTTPStatus. + self.simple_error(e.response.status_code, "unspecified server error") + except Exception as e: + logger.error(e) + self.simple_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, "internal proxy error") self.on_done(self.res) diff --git a/tests/files/dev-config.yaml b/tests/files/dev-config.yaml new file mode 100644 index 0000000..8e72283 --- /dev/null +++ b/tests/files/dev-config.yaml @@ -0,0 +1,5 @@ +host: jsonplaceholder.typicode.com +scheme: https +port: 443 +target_vm: compost +dev: True diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index d0993e4..83c861b 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -5,6 +5,8 @@ import unittest from unittest.mock import patch +import vcr + from securedrop_proxy import callbacks from securedrop_proxy import config from securedrop_proxy import proxy @@ -76,3 +78,52 @@ def test_on_save_200_success(self): 'application/json') self.assertEqual(self.res.status, 200) self.assertIn('filename', self.res.body) + + @vcr.use_cassette("fixtures/proxy_callbacks.yaml") + def test_custom_callbacks(self): + """ + Test the handlers in a real proxy request. + """ + conf = config.Conf() + conf.host = 'jsonplaceholder.typicode.com' + conf.scheme = 'https' + conf.port = 443 + + req = proxy.Req() + req.method = "GET" + + on_save_addition = "added by the on_save callback\n" + on_done_addition = "added by the on_done callback\n" + + def on_save(fh, res, conf): + res.headers['Content-Type'] = 'text/plain' + res.body = on_save_addition + + def on_done(res): + res.headers['Content-Type'] = 'text/plain' + res.body += on_done_addition + + p = proxy.Proxy(self.conf, req, on_save=on_save, on_done=on_done) + p.proxy() + + self.assertEqual( + p.res.body, + "{}{}".format(on_save_addition, on_done_addition) + ) + + @vcr.use_cassette("fixtures/proxy_callbacks.yaml") + def test_production_on_save(self): + """ + Test on_save's production file handling. + """ + conf = config.Conf() + conf.host = 'jsonplaceholder.typicode.com' + conf.scheme = 'https' + conf.port = 443 + conf.dev = False + conf.target_vm = "sd-svs-dispvm" + + with patch("subprocess.run") as patched_run: + fh = tempfile.NamedTemporaryFile() + callbacks.on_save(fh, self.res, conf) + self.assertEqual(patched_run.call_args[0][0][0], "qvm-move-to-vm") diff --git a/tests/test_config.py b/tests/test_config.py index 916ad67..0ae93ea 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -90,3 +90,7 @@ def err_on_done(res): with self.assertRaises(SystemExit): config.read_conf('tests/files/missing-target-vm.yaml', self.p) + + def test_dev_config(self): + c = config.read_conf('tests/files/dev-config.yaml', self.p) + self.assertTrue(c.dev) diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py new file mode 100644 index 0000000..8a5b0ea --- /dev/null +++ b/tests/test_entrypoint.py @@ -0,0 +1,129 @@ +import contextlib +import http +import io +import json +import os +import tempfile +import unittest.mock + +import vcr +from securedrop_proxy import entrypoint + + +@contextlib.contextmanager +def sdhome(*args, **kwds): + with tempfile.TemporaryDirectory() as tmphome: + os.environ["SECUREDROP_HOME"] = tmphome + try: + yield tmphome + finally: + del os.environ["SECUREDROP_HOME"] + + +class TestEntrypoint(unittest.TestCase): + """ + Test the entrypoint used in production. + """ + + def test_missing_config(self): + config_path = "/tmp/nonexistent-config.yaml" + self.assertFalse(os.path.exists(config_path)) + + output = None + with unittest.mock.patch( + "sys.argv", new_callable=lambda: ["sd-proxy", config_path] + ) as mock_argv, unittest.mock.patch( # noqa: F841 + "sys.stdout", new_callable=io.StringIO + ) as mock_stdout: + with self.assertRaises(SystemExit), sdhome(): + entrypoint.start() + output = mock_stdout.getvalue() + + response = json.loads(output) + self.assertEqual(response["status"], http.HTTPStatus.INTERNAL_SERVER_ERROR) + body = json.loads(response["body"]) + self.assertEqual( + body["error"], "Configuration file does not exist at {}".format(config_path) + ) + + def test_unwritable_log_folder(self): + """ + Tests a permission problem in `configure_logging`. + """ + output = None + with sdhome() as home: + os.chmod(home, 0o0444) + with unittest.mock.patch( + "sys.stdout", new_callable=io.StringIO + ) as mock_stdout: + with self.assertRaises(SystemExit): + entrypoint.start() + output = mock_stdout.getvalue() + os.chmod(home, 0o0700) + + response = json.loads(output) + self.assertEqual(response["status"], http.HTTPStatus.INTERNAL_SERVER_ERROR) + body = json.loads(response["body"]) + self.assertIn("Permission denied: ", body["error"]) + + def test_wrong_number_of_arguments(self): + with sdhome() as home: # noqa: F841 + with unittest.mock.patch( + "sys.argv", new_callable=lambda: ["sd-proxy"] + ) as mock_argv, unittest.mock.patch( # noqa: F841 + "sys.stdout", new_callable=io.StringIO + ) as mock_stdout: + with self.assertRaises(SystemExit): + entrypoint.start() + output = mock_stdout.getvalue() + + response = json.loads(output) + self.assertEqual(response["status"], http.HTTPStatus.INTERNAL_SERVER_ERROR) + body = json.loads(response["body"]) + self.assertEqual( + body["error"], "sd-proxy script not called with path to configuration file" + ) + + def test_empty_input(self): + config_path = "tests/files/valid-config.yaml" + self.assertTrue(os.path.exists(config_path)) + + with sdhome() as home: # noqa: F841 + with unittest.mock.patch( + "sys.stdin", new_callable=lambda: io.StringIO("") + ) as mock_stdin, unittest.mock.patch( # noqa: F841 + "sys.stdout", new_callable=io.StringIO + ) as mock_stdout, unittest.mock.patch( + "sys.argv", new_callable=lambda: ["sd-proxy", config_path] + ) as mock_argv: # noqa: F841 + entrypoint.start() + output = mock_stdout.getvalue() + + response = json.loads(output) + self.assertEqual(response["status"], http.HTTPStatus.BAD_REQUEST) + body = json.loads(response["body"]) + self.assertEqual(body["error"], "Invalid JSON in request") + + @vcr.use_cassette("fixtures/main_json_response.yaml") + def test_json_response(self): + config_path = "tests/files/valid-config.yaml" + self.assertTrue(os.path.exists(config_path)) + + test_input = { + "method": "GET", + "path_query": "/posts?userId=1", + } + + output = None + with sdhome() as home, unittest.mock.patch( # noqa: F841 + "sys.stdin", new_callable=lambda: io.StringIO(json.dumps(test_input)) + ) as mock_stding, unittest.mock.patch( # noqa: F841 + "sys.stdout", new_callable=io.StringIO + ) as mock_stdout, unittest.mock.patch( + "sys.argv", new_callable=lambda: ["sd-proxy", config_path] + ) as mock_argv: # noqa: F841 + entrypoint.start() + output = mock_stdout.getvalue() + + response = json.loads(output) + self.assertEqual(response["status"], http.HTTPStatus.OK) diff --git a/tests/test_main.py b/tests/test_main.py index 8a4a28e..c4e10e2 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,3 +1,4 @@ +import http from io import StringIO import json import subprocess @@ -5,6 +6,8 @@ import unittest import uuid +import vcr + from securedrop_proxy import config from securedrop_proxy import main from securedrop_proxy import proxy @@ -13,31 +16,30 @@ class TestMain(unittest.TestCase): def setUp(self): self.conf = config.Conf() - self.conf.host = 'jsonplaceholder.typicode.com' - self.conf.scheme = 'https' + self.conf.host = "jsonplaceholder.typicode.com" + self.conf.scheme = "https" self.conf.port = 443 self.conf.dev = True + @vcr.use_cassette("fixtures/main_json_response.yaml") def test_json_response(self): test_input_json = """{ "method": "GET", "path_query": "/posts?userId=1" }""" req = proxy.Req() - req.method = 'GET' - req.path_query = '' - req.headers = {'Accept': 'application/json'} + req.method = "GET" + req.path_query = "" + req.headers = {"Accept": "application/json"} # Use custom callbacks def on_save(res, fh, conf): pass def on_done(res): - res = res.__dict__ - self.assertEqual(res['status'], 200) + self.assertEqual(res.status, http.HTTPStatus.OK) + print(json.dumps(res.__dict__)) - self.p = proxy.Proxy(self.conf, req, on_save) - self.p.on_done = on_done - self.p.proxy() + self.p = proxy.Proxy(self.conf, req, on_save, on_done) saved_stdout = sys.stdout try: @@ -49,9 +51,10 @@ def on_done(res): sys.stdout = saved_stdout response = json.loads(output) - for item in json.loads(response['body']): - self.assertEqual(item['userId'], 1) + for item in json.loads(response["body"]): + self.assertEqual(item["userId"], 1) + @vcr.use_cassette("fixtures/main_non_json_response.yaml") def test_non_json_response(self): test_input_json = """{ "method": "GET", "path_query": "" }""" @@ -61,12 +64,11 @@ def on_save(fh, res, conf): subprocess.run(["cp", fh.name, "/tmp/{}".format(self.fn)]) - res.headers['X-Origin-Content-Type'] = res.headers['Content-Type'] - res.headers['Content-Type'] = 'application/json' - res.body = json.dumps({'filename': self.fn}) + res.headers["X-Origin-Content-Type"] = res.headers["Content-Type"] + res.headers["Content-Type"] = "application/json" + res.body = json.dumps({"filename": self.fn}) self.p = proxy.Proxy(self.conf, proxy.Req(), on_save) - self.p.proxy() saved_stdout = sys.stdout try: @@ -78,19 +80,19 @@ def on_save(fh, res, conf): sys.stdout = saved_stdout response = json.loads(output) - self.assertEqual(response['status'], 200) + self.assertEqual(response["status"], 200) # The proxy should have created a filename in the response body - self.assertIn('filename', response['body']) + self.assertIn("filename", response["body"]) # The file should not be empty with open("/tmp/{}".format(self.fn)) as f: saved_file = f.read() # We expect HTML content in the file from the test data - self.assertIn("", saved_file) + self.assertIn("", saved_file) - def test_error_response(self): + def test_input_invalid_json(self): test_input_json = """"foo": "bar", "baz": "bliff" }""" def on_save(fh, res, conf): @@ -98,12 +100,73 @@ def on_save(fh, res, conf): def on_done(res): res = res.__dict__ - self.assertEqual(res['status'], 400) + self.assertEqual(res["status"], 400) sys.exit(1) - self.p = proxy.Proxy(self.conf, proxy.Req(), on_save) - self.p.on_done = on_done + p = proxy.Proxy(self.conf, proxy.Req(), on_save, on_done) with self.assertRaises(SystemExit): - self.p.proxy() - main.__main__(test_input_json, self.p) + main.__main__(test_input_json, p) + + def test_input_missing_keys(self): + test_input_json = """{ "foo": "bar", "baz": "bliff" }""" + + def on_save(fh, res, conf): + pass + + def on_done(res): + res = res.__dict__ + self.assertEqual(res["status"], 400) + self.assertEqual(res["body"], '{"error": "Missing keys in request"}') + sys.exit(1) + + p = proxy.Proxy(self.conf, proxy.Req(), on_save, on_done) + with self.assertRaises(SystemExit): + main.__main__(test_input_json, p) + + @vcr.use_cassette("fixtures/main_input_headers.yaml") + def test_input_headers(self): + test_input = { + "method": "GET", + "path_query": "/posts?userId=1", + "headers": {"X-Test-Header": "th"}, + } + + def on_save(fh, res, conf): + pass + + p = proxy.Proxy(self.conf, proxy.Req(), on_save) + main.__main__(json.dumps(test_input), p) + self.assertEqual(p.req.headers, test_input["headers"]) + + @vcr.use_cassette("fixtures/main_input_body.yaml") + def test_input_body(self): + test_input = { + "method": "POST", + "path_query": "/posts", + "body": {"id": 42, "title": "test"}, + } + + def on_save(fh, res, conf): + pass + + p = proxy.Proxy(self.conf, proxy.Req(), on_save) + main.__main__(json.dumps(test_input), p) + self.assertEqual(p.req.body, test_input["body"]) + + @vcr.use_cassette("fixtures/main_non_json_response.yaml") + def test_default_callbacks(self): + test_input = { + "method": "GET", + "path_query": "", + } + + p = proxy.Proxy(self.conf, proxy.Req()) + with unittest.mock.patch( + "securedrop_proxy.callbacks.on_done" + ) as on_done, unittest.mock.patch( + "securedrop_proxy.callbacks.on_save" + ) as on_save: + main.__main__(json.dumps(test_input), p) + self.assertEqual(on_save.call_count, 1) + self.assertEqual(on_done.call_count, 1) diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 6278dce..bc73b05 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -1,8 +1,12 @@ +import http import json -import vcr import unittest import uuid +import requests +import vcr + +from securedrop_proxy import callbacks from securedrop_proxy import proxy from securedrop_proxy import config from securedrop_proxy import version @@ -136,7 +140,7 @@ def test_proxy_400_no_handler(self): self.assertEqual(p.res.status, 400) self.assertEqual(p.res.headers['Content-Type'], 'application/json') - self.assertIn('Request callback is not set', + self.assertIn('Request on_save callback is not set', p.res.body) @@ -167,3 +171,127 @@ def test_proxy_500_misconfiguration(self): self.assertEqual(p.res.headers['Content-Type'], 'application/json') self.assertIn('Proxy error while generating URL to request', p.res.body) + + +class TestServerErrorHandling(unittest.TestCase): + def setUp(self): + self.conf = config.Conf() + self.conf.host = "localhost" + self.conf.scheme = "http" + self.conf.port = 8000 + + def make_request(self, method="GET", path_query="/", headers=None): + req = proxy.Req() + req.method = method if method else "GET" + req.path_query = path_query if path_query else "/" + req.headers = headers if headers else {"Accept": "application/json"} + return req + + def test_cannot_connect(self): + """ + Test for "502 Bad Gateway" when the server can't be reached. + """ + req = self.make_request() + + conf = config.Conf() + conf.host = "sdproxytest.local" + conf.scheme = "https" + conf.port = 8000 + + p = proxy.Proxy(conf, req, on_save=callbacks.on_save) + p.proxy() + + self.assertEqual(p.res.status, http.HTTPStatus.BAD_GATEWAY) + self.assertIn("application/json", p.res.headers["Content-Type"]) + body = json.loads(p.res.body) + self.assertEqual(body["error"], "could not connect to server") + + def test_server_timeout(self): + """ + Test for "504 Gateway Timeout" when the server times out. + """ + class TimeoutProxy(proxy.Proxy): + """ + Mocks a slow upstream server. + + VCR cassettes cannot represent a request that takes too + long. This Proxy subclass raises the exception that would + cause. + """ + def prep_request(self): + raise requests.exceptions.Timeout('test timeout') + + req = self.make_request(path_query="/tarpit") + p = TimeoutProxy(self.conf, req, on_save=callbacks.on_save, timeout=0.00001) + p.proxy() + + self.assertEqual(p.res.status, http.HTTPStatus.GATEWAY_TIMEOUT) + self.assertIn("application/json", p.res.headers["Content-Type"]) + body = json.loads(p.res.body) + self.assertEqual(body["error"], "request timed out") + + @vcr.use_cassette("fixtures/proxy_bad_request.yaml") + def test_bad_request(self): + """ + Test handling of "400 Bad Request" from the server. + """ + req = self.make_request(path_query="/bad") + p = proxy.Proxy(self.conf, req, on_save=callbacks.on_save) + p.proxy() + + self.assertEqual(p.res.status, http.HTTPStatus.BAD_REQUEST) + self.assertIn("application/json", p.res.headers["Content-Type"]) + body = json.loads(p.res.body) + self.assertEqual(body["error"], http.HTTPStatus.BAD_REQUEST.phrase.lower()) + + @vcr.use_cassette("fixtures/proxy_unofficial_status.yaml") + def test_unofficial_status(self): + """ + Make sure we handle unofficial status codes from the server. + + Should the server ever need to return a status code not in + Python's http.HTTPStatus, the proxy should still return a + proper JSON error response with a generic error message. + """ + req = self.make_request(path_query="/teapot") + p = proxy.Proxy(self.conf, req, on_save=callbacks.on_save) + p.proxy() + + self.assertEqual(p.res.status, 418) + self.assertIn("application/json", p.res.headers["Content-Type"]) + body = json.loads(p.res.body) + self.assertEqual(body["error"], "unspecified server error") + + @vcr.use_cassette("fixtures/proxy_internal_server_error.yaml") + def test_internal_server_error(self): + """ + Test handling of "500 Internal Server Error" from the server. + """ + req = self.make_request(path_query="/crash") + p = proxy.Proxy(self.conf, req, on_save=callbacks.on_save) + p.proxy() + + self.assertEqual(p.res.status, http.HTTPStatus.INTERNAL_SERVER_ERROR) + self.assertIn("application/json", p.res.headers["Content-Type"]) + body = json.loads(p.res.body) + self.assertEqual( + body["error"], + http.HTTPStatus.INTERNAL_SERVER_ERROR.phrase.lower() + ) + + @vcr.use_cassette("fixtures/proxy_internal_error.yaml") + def test_internal_error(self): + """ + Ensure that the proxy returns JSON despite internal errors. + """ + def bad_on_save(self, fh, res, conf): + raise Exception("test internal proxy error") + + req = self.make_request() + p = proxy.Proxy(self.conf, req, on_save=bad_on_save) + p.proxy() + + self.assertEqual(p.res.status, http.HTTPStatus.INTERNAL_SERVER_ERROR) + self.assertIn("application/json", p.res.headers["Content-Type"]) + body = json.loads(p.res.body) + self.assertEqual(body["error"], "internal proxy error")