aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.coveragerc14
-rw-r--r--Pipfile5
-rw-r--r--Pipfile.lock173
-rw-r--r--snekbox/api/__init__.py3
-rw-r--r--snekbox/api/app.py3
-rw-r--r--snekbox/api/middleware/__init__.py3
-rw-r--r--snekbox/api/middleware/logger.py11
-rw-r--r--snekbox/api/resources/__init__.py3
-rw-r--r--snekbox/api/resources/eval.py74
-rw-r--r--snekbox/api/snekapi.py27
-rw-r--r--snekbox/site/snekapp.py35
-rw-r--r--snekbox/site/templates/index.html14
-rw-r--r--snekbox/site/templates/result.html9
-rw-r--r--tests/__init__.py1
-rw-r--r--tests/api/__init__.py17
-rw-r--r--tests/api/test_eval.py49
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/*
diff --git a/Pipfile b/Pipfile
index 788e900..72df21b 100644
--- a/Pipfile
+++ b/Pipfile
@@ -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
-
-
-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")