diff options
-rw-r--r-- | .coveragerc | 14 | ||||
-rw-r--r-- | Pipfile | 5 | ||||
-rw-r--r-- | Pipfile.lock | 173 | ||||
-rw-r--r-- | snekbox/api/__init__.py | 3 | ||||
-rw-r--r-- | snekbox/api/app.py | 3 | ||||
-rw-r--r-- | snekbox/api/middleware/__init__.py | 3 | ||||
-rw-r--r-- | snekbox/api/middleware/logger.py | 11 | ||||
-rw-r--r-- | snekbox/api/resources/__init__.py | 3 | ||||
-rw-r--r-- | snekbox/api/resources/eval.py | 74 | ||||
-rw-r--r-- | snekbox/api/snekapi.py | 27 | ||||
-rw-r--r-- | snekbox/site/snekapp.py | 35 | ||||
-rw-r--r-- | snekbox/site/templates/index.html | 14 | ||||
-rw-r--r-- | snekbox/site/templates/result.html | 9 | ||||
-rw-r--r-- | tests/__init__.py | 1 | ||||
-rw-r--r-- | tests/api/__init__.py | 17 | ||||
-rw-r--r-- | tests/api/test_eval.py | 49 |
16 files changed, 278 insertions, 163 deletions
diff --git a/.coveragerc b/.coveragerc index c1e718b..d1877d4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,10 +1,6 @@ [run] -omit = .venv/*, - tests/*, - snekweb.py - config.py - -[report] -exclude_lines = return jsonify, - raise RuntimeError, - return +branch = True +omit = + .venv/*, + snekbox/api/app.py, + tests/* @@ -4,8 +4,9 @@ verify_ssl = true name = "pypi" [packages] -flask = "*" +falcon = "*" gunicorn = "*" +jsonschema = "*" [dev-packages] pytest = "*" @@ -29,7 +30,7 @@ lint = "flake8" precommit = "pre-commit install" test = "pytest tests --cov . --cov-report term-missing -v" report = "pytest tests --cov . --cov-report=html" -snekbox = "gunicorn -w 2 -b 0.0.0.0:8060 snekbox.site.snekapp:app" +snekbox = "gunicorn -w 2 -b 0.0.0.0:8060 snekbox.api.app" buildbox = "docker build -t pythondiscord/snekbox:latest -f docker/Dockerfile ." pushbox = "docker push pythondiscord/snekbox:latest" buildboxbase = "docker build -t pythondiscord/snekbox-base:latest -f docker/base.Dockerfile ." diff --git a/Pipfile.lock b/Pipfile.lock index 466a42b..e0c08a0 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "814185e2e1b964ab58af9a9df416ace7b5b416475d828ec9b31a9dfecb5693e1" + "sha256": "8dda3bfb1f2f9109b882225e4c55e3a561e25a7d80845889b4ffe5ad250cc86e" }, "pipfile-spec": 6, "requires": { @@ -16,20 +16,32 @@ ] }, "default": { - "click": { + "attrs": { "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" ], - "version": "==7.0" + "version": "==19.1.0" }, - "flask": { - "hashes": [ - "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", - "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" + "falcon": { + "hashes": [ + "sha256:18157af2a4fc3feedf2b5dcc6196f448639acf01c68bc33d4d5a04c3ef87f494", + "sha256:24adcd2b29a8ffa9d552dc79638cd21736a3fb04eda7d102c6cebafdaadb88ad", + "sha256:54f2cb4b687035b2a03206dbfc538055cc48b59a953187b0458aa1b574d47b53", + "sha256:59d1e8c993b9a37ea06df9d72cf907a46cc8063b30717cdac2f34d1658b6f936", + "sha256:733033ec80c896e30a43ab3e776856096836787197a44eb21022320a61311983", + "sha256:74cf1d18207381c665b9e6292d65100ce146d958707793174b03869dc6e614f4", + "sha256:95bf6ce986c1119aef12c9b348f4dee9c6dcc58391bdd0bc2b0bf353c2b15986", + "sha256:9712975adcf8c6e12876239085ad757b8fdeba223d46d23daef82b47658f83a9", + "sha256:a5ebb22a04c9cc65081938ee7651b4e3b4d2a28522ea8ec04c7bdd2b3e9e8cd8", + "sha256:aa184895d1ad4573fbfaaf803563d02f019ebdf4790e41cc568a330607eae439", + "sha256:e3782b7b92fefd46a6ad1fd8fe63fe6c6f1b7740a95ca56957f48d1aee34b357", + "sha256:e9efa0791b5d9f9dd9689015ea6bce0a27fcd5ecbcd30e6d940bffa4f7f03389", + "sha256:eea593cf466b9c126ce667f6d30503624ef24459f118c75594a69353b6c3d5fc", + "sha256:f93351459f110b4c1ee28556aef9a791832df6f910bea7b3f616109d534df06b" ], "index": "pypi", - "version": "==1.0.2" + "version": "==2.0.0" }, "gunicorn": { "hashes": [ @@ -39,68 +51,36 @@ "index": "pypi", "version": "==19.9.0" }, - "itsdangerous": { - "hashes": [ - "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", - "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" - ], - "version": "==1.1.0" - }, - "jinja2": { - "hashes": [ - "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", - "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" - ], - "version": "==2.10" - }, - "markupsafe": { - "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" - ], - "version": "==1.1.1" - }, - "werkzeug": { - "hashes": [ - "sha256:96da23fa8ccecbc3ae832a83df5c722c11547d021637faacb0bec4dd2f4666c8", - "sha256:ca5c2dcd367d6c0df87185b9082929d255358f5391923269335782b213d52655" - ], - "version": "==0.15.1" + "jsonschema": { + "hashes": [ + "sha256:0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d", + "sha256:a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a" + ], + "index": "pypi", + "version": "==3.0.1" + }, + "pyrsistent": { + "hashes": [ + "sha256:16692ee739d42cf5e39cef8d27649a8c1fdb7aa99887098f1460057c5eb75c3a", + "sha256:1b304bab4d25fbe2b5bf32034d0d904bfc870f7f4ed9dccec6ae388978f0ef6f" + ], + "version": "==0.15.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" } }, "develop": { "aspy.yaml": { "hashes": [ - "sha256:ae249074803e8b957c83fdd82a99160d0d6d26dff9ba81ba608b42eebd7d8cd3", - "sha256:c7390d79f58eb9157406966201abf26da0d56c07e0ff0deadc39c8f4dbc13482" + "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", + "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" ], - "version": "==1.2.0" + "version": "==1.3.0" }, "atomicwrites": { "hashes": [ @@ -118,10 +98,10 @@ }, "cfgv": { "hashes": [ - "sha256:39f8475d8eca48639f900daffa3f8bd2f60a31d989df41a9f81c5ad1779a66eb", - "sha256:a6a4366d32799a6bfb6f577ebe113b27ba8d1bae43cb57133b1472c1c3dae227" + "sha256:32edbe09de6f4521224b87822103a8c16a614d31a894735f7a5b3bcf0eb3c37e", + "sha256:3bd31385cd2bebddbba8012200aaf15aa208539f1b33973759b4d02fc2148da5" ], - "version": "==1.5.0" + "version": "==2.0.0" }, "coverage": { "hashes": [ @@ -238,17 +218,17 @@ }, "identify": { "hashes": [ - "sha256:244e7864ef59f0c7c50c6db73f58564151d91345cd9b76ed793458953578cadd", - "sha256:8ff062f90ad4b09cfe79b5dfb7a12e40f19d2e68a5c9598a49be45f16aba7171" + "sha256:432c548d6138cb57a3d8f62f079a025a29b8ae34a50dd3b496bbf661818f2bc0", + "sha256:d4401d60bf1938aa3074a352a5cc9044107edf11a6fedd3a1db172c141619b81" ], - "version": "==1.4.1" + "version": "==1.4.3" }, "importlib-metadata": { "hashes": [ - "sha256:a17ce1a8c7bff1e8674cb12c992375d8d0800c9190177ecf0ad93e0097224095", - "sha256:b50191ead8c70adfa12495fba19ce6d75f2e0275c14c5a7beb653d6799b512bd" + "sha256:027cfc6524613de726789072f95d2e4cc64dd1dee8096d42d13f2ead5bd302f5", + "sha256:0d05199e1f0b1a8707a1b9c46476d4a49807fb56cb1b0737db1d37feb42fe31d" ], - "version": "==0.8" + "version": "==0.15" }, "junit-xml": { "hashes": [ @@ -279,18 +259,18 @@ }, "pluggy": { "hashes": [ - "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", - "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" + "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", + "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c" ], - "version": "==0.9.0" + "version": "==0.12.0" }, "pre-commit": { "hashes": [ - "sha256:d3d69c63ae7b7584c4b51446b0b583d454548f9df92575b2fe93a68ec800c4d3", - "sha256:fc512f129b9526e35e80d656a16a31c198f584c4fce3a5c739045b5140584917" + "sha256:6ca409d1f22d444af427fb023a33ca8b69625d508a50e1b7eaabd59247c93043", + "sha256:94dd519597f5bff06a4b0df194a79c524b78f4b1534c1ce63241a9d4fb23b926" ], "index": "pypi", - "version": "==1.14.4" + "version": "==1.16.1" }, "py": { "hashes": [ @@ -323,19 +303,19 @@ }, "pytest": { "hashes": [ - "sha256:592eaa2c33fae68c7d75aacf042efc9f77b27c08a6224a4f59beab8d9a420523", - "sha256:ad3ad5c450284819ecde191a654c09b0ec72257a2c711b9633d677c71c9850c4" + "sha256:1a8aa4fa958f8f451ac5441f3ac130d9fc86ea38780dd2715e6d5c5882700b24", + "sha256:b8bf138592384bd4e87338cb0f256bf5f615398a649d4bd83915f0e4047a5ca6" ], "index": "pypi", - "version": "==4.3.1" + "version": "==4.5.0" }, "pytest-cov": { "hashes": [ - "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", - "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f" + "sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6", + "sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a" ], "index": "pypi", - "version": "==2.6.1" + "version": "==2.7.1" }, "pytest-dependency": { "hashes": [ @@ -383,17 +363,24 @@ }, "virtualenv": { "hashes": [ - "sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417", - "sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39" + "sha256:99acaf1e35c7ccf9763db9ba2accbca2f4254d61d1912c5ee364f9cc4a8942a0", + "sha256:fe51cdbf04e5d8152af06c075404745a7419de27495a83f0d72518ad50be3ce8" + ], + "version": "==16.6.0" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" ], - "version": "==16.4.3" + "version": "==0.1.7" }, "zipp": { "hashes": [ - "sha256:55ca87266c38af6658b84db8cfb7343cdb0bf275f93c7afaea0d8e7a209c7478", - "sha256:682b3e1c62b7026afe24eadf6be579fb45fec54c07ea218bded8092af07a68c4" + "sha256:8c1019c6aad13642199fbe458275ad6a84907634cc9f0989877ccc4a2840139d", + "sha256:ca943a7e809cc12257001ccfb99e3563da9af99d52f261725e96dfe0f9275bc3" ], - "version": "==0.3.3" + "version": "==0.5.1" } } } diff --git a/snekbox/api/__init__.py b/snekbox/api/__init__.py new file mode 100644 index 0000000..d106f26 --- /dev/null +++ b/snekbox/api/__init__.py @@ -0,0 +1,3 @@ +from .snekapi import SnekAPI + +__all__ = ("SnekAPI",) diff --git a/snekbox/api/app.py b/snekbox/api/app.py new file mode 100644 index 0000000..c71e246 --- /dev/null +++ b/snekbox/api/app.py @@ -0,0 +1,3 @@ +from . import SnekAPI + +application = SnekAPI() diff --git a/snekbox/api/middleware/__init__.py b/snekbox/api/middleware/__init__.py new file mode 100644 index 0000000..ba97ca6 --- /dev/null +++ b/snekbox/api/middleware/__init__.py @@ -0,0 +1,3 @@ +from .logger import LoggingMiddleware + +__all__ = ("LoggingMiddleware",) diff --git a/snekbox/api/middleware/logger.py b/snekbox/api/middleware/logger.py new file mode 100644 index 0000000..393fc64 --- /dev/null +++ b/snekbox/api/middleware/logger.py @@ -0,0 +1,11 @@ +import logging + +log = logging.getLogger("snekbox.api") + + +class LoggingMiddleware: + """Log basic information about responses.""" + + def process_response(self, req, resp, resource, req_succeeded): + """Log the method, route, and status of a response.""" + log.info(f"{req.method} {req.relative_uri} {resp.status}") diff --git a/snekbox/api/resources/__init__.py b/snekbox/api/resources/__init__.py new file mode 100644 index 0000000..fe422b8 --- /dev/null +++ b/snekbox/api/resources/__init__.py @@ -0,0 +1,3 @@ +from .eval import EvalResource + +__all__ = ("EvalResource",) diff --git a/snekbox/api/resources/eval.py b/snekbox/api/resources/eval.py new file mode 100644 index 0000000..b2f4260 --- /dev/null +++ b/snekbox/api/resources/eval.py @@ -0,0 +1,74 @@ +import logging + +import falcon +from falcon.media.validators.jsonschema import validate + +from snekbox.nsjail import NsJail + +log = logging.getLogger(__name__) + + +class EvalResource: + """ + Evaluation of Python code. + + Supported methods: + + - POST /eval + Evaluate Python code and return the result + """ + + REQ_SCHEMA = { + "type": "object", + "properties": { + "input": { + "type": "string" + } + }, + "required": [ + "input" + ] + } + + def __init__(self): + self.nsjail = NsJail() + + @validate(REQ_SCHEMA) + def on_post(self, req, resp): + """ + Evaluate Python code and return the result. + + Request body: + + >>> { + ... "input": "print(1 + 1)" + ... } + + Response format: + + >>> { + ... "input": "print(1 + 1)", + ... "output": "2\\n" + ... } + + Status codes: + + - 200 + Successful evaluation; not indicative that the input code itself works + - 400 + Input's JSON schema is invalid + - 415 + Unsupported content type; only application/JSON is supported + """ + code = req.media["input"] + + try: + output = self.nsjail.python3(code) + except Exception: + log.exception("An exception occurred while trying to process the request") + raise falcon.HTTPInternalServerError + + resp.media = { + "input": code, + "output": output + } diff --git a/snekbox/api/snekapi.py b/snekbox/api/snekapi.py new file mode 100644 index 0000000..849e7d6 --- /dev/null +++ b/snekbox/api/snekapi.py @@ -0,0 +1,27 @@ +import falcon + +from .middleware import LoggingMiddleware +from .resources import EvalResource + + +class SnekAPI(falcon.API): + """ + The main entry point to the snekbox JSON API. + + Routes: + + - /eval + Evaluation of Python code + + Error response format: + + >>> { + ... "title": "Unsupported media type", + ... "description": "application/xml is an unsupported media type." + ... } + """ + + def __init__(self, *args, **kwargs): + super().__init__(middleware=[LoggingMiddleware()], *args, **kwargs) + + self.add_route("/eval", EvalResource()) diff --git a/snekbox/site/snekapp.py b/snekbox/site/snekapp.py deleted file mode 100644 index ef96148..0000000 --- a/snekbox/site/snekapp.py +++ /dev/null @@ -1,35 +0,0 @@ -from flask import Flask, jsonify, render_template, request - -from snekbox.nsjail import NsJail - -nsjail = NsJail() - -# Load app -app = Flask(__name__) -app.use_reloader = False - -# Logging -log = app.logger - - [email protected]('/') -def index(): - """Return a page with a form for inputting code to be executed.""" - return render_template('index.html') - - [email protected]('/result', methods=["POST", "GET"]) -def result(): - """Execute code and return a page displaying the results.""" - if request.method == "POST": - code = request.form["Code"] - output = nsjail.python3(code) - return render_template('result.html', code=code, result=output) - - [email protected]('/input', methods=["POST"]) -def code_input(): - """Execute code and return the results.""" - body = request.get_json() - output = nsjail.python3(body["code"]) - return jsonify(input=body["code"], output=output) diff --git a/snekbox/site/templates/index.html b/snekbox/site/templates/index.html deleted file mode 100644 index 41980d1..0000000 --- a/snekbox/site/templates/index.html +++ /dev/null @@ -1,14 +0,0 @@ -<!DOCTYPE html> -<html> - <meta charset="utf-8" /> - <title>snekboxweb</title> - <body> - <form action="/result" method="POST"> - <p>Code:<br><textarea name="Code" rows="20" cols="80"> -def sum(a,b): - return a+b -print( sum(1,2) )</textarea></p> - <p><input type="submit" value="Run"></p> - </form> - </body> -</html> diff --git a/snekbox/site/templates/result.html b/snekbox/site/templates/result.html deleted file mode 100644 index e339605..0000000 --- a/snekbox/site/templates/result.html +++ /dev/null @@ -1,9 +0,0 @@ -<!DOCTYPE html> -<html> - <meta charset="utf-8" /> - <title>snekboxweb</title> - <body> - <p>Code Evaluated:<br><pre>{{ code }}</pre></p> - <p>Results:<br><pre>{{ result }}</pre></p> - </body> -</html> diff --git a/tests/__init__.py b/tests/__init__.py index 792d600..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +0,0 @@ -# diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..fd4679a --- /dev/null +++ b/tests/api/__init__.py @@ -0,0 +1,17 @@ +from unittest import mock + +from falcon import testing + +from snekbox.api import SnekAPI + + +class SnekAPITestCase(testing.TestCase): + def setUp(self): + super().setUp() + + self.patcher = mock.patch("snekbox.api.resources.eval.NsJail", autospec=True) + self.mock_nsjail = self.patcher.start() + self.mock_nsjail.return_value.python3.return_value = "test output" + self.addCleanup(self.patcher.stop) + + self.app = SnekAPI() diff --git a/tests/api/test_eval.py b/tests/api/test_eval.py new file mode 100644 index 0000000..a5b83fd --- /dev/null +++ b/tests/api/test_eval.py @@ -0,0 +1,49 @@ +from tests.api import SnekAPITestCase + + +class TestEvalResource(SnekAPITestCase): + PATH = "/eval" + + def test_post_valid_200(self): + body = {"input": "foo"} + result = self.simulate_post(self.PATH, json=body) + + self.assertEqual(result.status_code, 200) + self.assertEqual(body["input"], result.json["input"]) + self.assertEqual("test output", result.json["output"]) + + def test_post_invalid_schema_400(self): + body = {"stuff": "foo"} + result = self.simulate_post(self.PATH, json=body) + + self.assertEqual(result.status_code, 400) + + expected = { + "title": "Request data failed validation", + "description": "'input' is a required property" + } + + self.assertEqual(expected, result.json) + + def test_post_invalid_content_type_415(self): + body = "{\"input\": \"foo\"}" + headers = {"Content-Type": "application/xml"} + result = self.simulate_post(self.PATH, body=body, headers=headers) + + self.assertEqual(result.status_code, 415) + + expected = { + "title": "Unsupported media type", + "description": "application/xml is an unsupported media type." + } + + self.assertEqual(expected, result.json) + + def test_disallowed_method_405(self): + result = self.simulate_get(self.PATH) + self.assertEqual(result.status_code, 405) + + def test_options_allow_post_only(self): + result = self.simulate_options(self.PATH) + self.assertEqual(result.status_code, 200) + self.assertEqual(result.headers.get("Allow"), "POST") |