diff --git a/README.md b/README.md index 8abd3e5..a2130ae 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,19 @@ ### Development dependencies -#### Python Dev-deps +#### Python dependencies +- `quart`: a micro-webframework, async version of Flask. - `black`: Code formatter - `isort`: Import formatter - `mypy`: Type checking - `flake8`: General Python bugs - `vulture`: Find unused code in Python programs +- `pytest`: For testing (turbocharged with `async`) +- `bcrypt`: Hashing and salting password. +- `zxcvbn`: Test password strength. +- `freezegun`: Check for expired token. +- `quart-rate-limiter`: rate limiting #### SQL Dev-deps diff --git a/backend/development.env b/backend/development.env index 1d1d497..d0c98bb 100644 --- a/backend/development.env +++ b/backend/development.env @@ -1,3 +1,7 @@ TODO_BASE_URL="localhost:5050" TODO_DEBUG=true TODO_SECRET_KEY="secret key" + +# disable for when we not using HTTPS +TODO_QUART_AUTH_COOKIE_SECURE=false +TODO_QUART_AUTH_COOKIE_SAMESITE="Strict" diff --git a/backend/pdm.lock b/backend/pdm.lock index 690cb00..61e32d0 100644 --- a/backend/pdm.lock +++ b/backend/pdm.lock @@ -22,6 +22,12 @@ dependencies = [ "stevedore>=1.20.0", ] +[[package]] +name = "bcrypt" +version = "4.0.1" +requires_python = ">=3.6" +summary = "Modern password hashing for your software and your servers" + [[package]] name = "black" version = "22.10.0" @@ -78,6 +84,15 @@ dependencies = [ "pyflakes<2.6.0,>=2.5.0", ] +[[package]] +name = "freezegun" +version = "1.2.2" +requires_python = ">=3.6" +summary = "Let your Python tests travel through time" +dependencies = [ + "python-dateutil>=2.7", +] + [[package]] name = "gitdb" version = "4.0.9" @@ -272,6 +287,15 @@ dependencies = [ "pytest>=6.1.0", ] +[[package]] +name = "python-dateutil" +version = "2.8.2" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +dependencies = [ + "six>=1.5", +] + [[package]] name = "pyyaml" version = "6.0" @@ -294,6 +318,30 @@ dependencies = [ "werkzeug>=2.2.0", ] +[[package]] +name = "quart-auth" +version = "0.7.0" +requires_python = ">=3.7" +summary = "A Quart extension to provide secure cookie authentication" +dependencies = [ + "quart>=0.18", +] + +[[package]] +name = "quart-rate-limiter" +version = "0.7.0" +requires_python = ">=3.7" +summary = "A Quart extension to provide rate limiting support" +dependencies = [ + "quart>=0.15", +] + +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" + [[package]] name = "smmap" version = "5.0.0" @@ -356,7 +404,7 @@ dependencies = [ [metadata] lock_version = "4.0" -content_hash = "sha256:f0267acbf584dfd2411f20fc3f63a5d42aa0bc85b37355aa528c0df8dda99498" +content_hash = "sha256:827a57e737ffa5147e1957e97f693d911838395691fd9fec9c2d93387e08cfb5" [metadata.files] "aiofiles 22.1.0" = [ @@ -371,6 +419,29 @@ content_hash = "sha256:f0267acbf584dfd2411f20fc3f63a5d42aa0bc85b37355aa528c0df8d {url = "https://files.pythonhosted.org/packages/39/36/a37a2f6f8d0ed8c3bc616616ed5019e1df2680bd8b7df49ceae80fd457de/bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"}, {url = "https://files.pythonhosted.org/packages/da/eb/ff828f4ec32c85e10d9c344e6b7f11bcacfb5d70f2fd16bea6fc1ae6df06/bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a"}, ] +"bcrypt 4.0.1" = [ + {url = "https://files.pythonhosted.org/packages/13/68/f3184c1f15581ebd936125b4da04cba0995f97ecd5ee8f4262c8ebba2646/bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, + {url = "https://files.pythonhosted.org/packages/28/ed/3c443bfbfdb37cd7c0d055b961311f49049ab4a00f45ba3bfd10d33a9443/bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, + {url = "https://files.pythonhosted.org/packages/2c/be/376341b47e1e3fc424c9df1af60b5aedbd5ab04f73ccdf4107e42d92ef09/bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, + {url = "https://files.pythonhosted.org/packages/41/16/49ff5146fb815742ad58cafb5034907aa7f166b1344d0ddd7fd1c818bd17/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, + {url = "https://files.pythonhosted.org/packages/41/86/05248719aa42a4fe1ca379d45794198700e992b91d389bfaa69533fc3331/bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, + {url = "https://files.pythonhosted.org/packages/46/81/d8c22cd7e5e1c6a7d48e41a1d1d46c92f17dae70a54d9814f746e6027dec/bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, + {url = "https://files.pythonhosted.org/packages/5e/01/098b798dc6c6984f2d5026269e80d7cad22d6ecacd5989bdf35a9c99a03d/bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, + {url = "https://files.pythonhosted.org/packages/64/fe/da28a5916128d541da0993328dc5cf4b43dfbf6655f2c7a2abe26ca2dc88/bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, + {url = "https://files.pythonhosted.org/packages/77/2c/53c17079898584306eafdc937e0c7cc1bf8e2fe17e9909716ef3f9d6555d/bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, + {url = "https://files.pythonhosted.org/packages/78/d4/3b2657bd58ef02b23a07729b0df26f21af97169dbd0b5797afa9e97ebb49/bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, + {url = "https://files.pythonhosted.org/packages/7d/50/e683d8418974a602ba40899c8a5c38b3decaf5a4d36c32fc65dce454d8a8/bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, + {url = "https://files.pythonhosted.org/packages/87/69/edacb37481d360d06fc947dab5734aaf511acb7d1a1f9e2849454376c0f8/bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, + {url = "https://files.pythonhosted.org/packages/8c/ae/3af7d006aacf513975fd1948a6b4d6f8b4a307f8a244e1a3d3774b297aad/bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, + {url = "https://files.pythonhosted.org/packages/99/a5/ff4aaf2adbefb2c9808d49cec37f65e0572c4ce856b13b194fd87a6cbd14/bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, + {url = "https://files.pythonhosted.org/packages/aa/48/fd2b197a9741fa790ba0b88a9b10b5e88e62ff5cf3e1bc96d8354d7ce613/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, + {url = "https://files.pythonhosted.org/packages/aa/ca/6a534669890725cbb8c1fb4622019be31813c8edaa7b6d5b62fc9360a17e/bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, + {url = "https://files.pythonhosted.org/packages/d8/f6/43ade4d37a3319baee9aec53f636411e70c18f0e4add9cc44a18f517af5f/bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, + {url = "https://files.pythonhosted.org/packages/dd/4f/3632a69ce344c1551f7c9803196b191a8181c6a1ad2362c225581ef0d383/bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, + {url = "https://files.pythonhosted.org/packages/ec/0a/1582790232fef6c2aa201f345577306b8bfe465c2c665dec04c86a016879/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, + {url = "https://files.pythonhosted.org/packages/fb/4b/e255df2000c2de4df524740b5f1d0a31157a1f7715b3eaf2e8f9c5c0acbb/bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, + {url = "https://files.pythonhosted.org/packages/fb/a7/ee4561fd9b78ca23c8e5591c150cc58626a5dfb169345ab18e1c2c664ee0/bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, +] "black 22.10.0" = [ {url = "https://files.pythonhosted.org/packages/2c/11/f2737cd3b458d91401801e83a014e87c63e8904dc063200f77826c352f54/black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, {url = "https://files.pythonhosted.org/packages/3d/c5/b3ab9b563f35fb284d37ab2b14acaed9a27d8cdea9c31364766eb54946a7/black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, @@ -417,6 +488,10 @@ content_hash = "sha256:f0267acbf584dfd2411f20fc3f63a5d42aa0bc85b37355aa528c0df8d {url = "https://files.pythonhosted.org/packages/ad/00/9808c62b2d529cefc69ce4e4a1ea42c0f855effa55817b7327ec5b75e60a/flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, {url = "https://files.pythonhosted.org/packages/cf/a0/b881b63a17a59d9d07f5c0cc91a29182c8e8a9aa2bde5b3b2b16519c02f4/flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, ] +"freezegun 1.2.2" = [ + {url = "https://files.pythonhosted.org/packages/1d/97/002ac49ec52858538b4aa6f6831f83c2af562c17340bdf6043be695f39ac/freezegun-1.2.2.tar.gz", hash = "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446"}, + {url = "https://files.pythonhosted.org/packages/50/cd/ba1c8319c002727ccfa03049127218d1767232a77219924d03ba170e0601/freezegun-1.2.2-py3-none-any.whl", hash = "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f"}, +] "gitdb 4.0.9" = [ {url = "https://files.pythonhosted.org/packages/a3/7c/5d747655049bfbf75b5fcec57c8115896cb78d6fafa84f6d3ef4c0f13a98/gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, {url = "https://files.pythonhosted.org/packages/fc/44/64e02ef96f20b347385f0e9c03098659cb5a1285d36c3d17c56e534d80cf/gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, @@ -587,6 +662,10 @@ content_hash = "sha256:f0267acbf584dfd2411f20fc3f63a5d42aa0bc85b37355aa528c0df8d {url = "https://files.pythonhosted.org/packages/65/57/fb0d22ee0d9cd8d8b277cdf4fd7efa3e245c9577d2c2d54c6c2c18b535a2/pytest_asyncio-0.20.2-py3-none-any.whl", hash = "sha256:07e0abf9e6e6b95894a39f688a4a875d63c2128f76c02d03d16ccbc35bcc0f8a"}, {url = "https://files.pythonhosted.org/packages/eb/db/479f1c43df26c42c9797dfc866dc98bcf28d3765ae49bf2da681ab344b72/pytest-asyncio-0.20.2.tar.gz", hash = "sha256:32a87a9836298a881c0ec637ebcc952cfe23a56436bdc0d09d1511941dd8a812"}, ] +"python-dateutil 2.8.2" = [ + {url = "https://files.pythonhosted.org/packages/36/7a/87837f39d0296e723bb9b62bbb257d0355c7f6128853c78955f57342a56d/python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {url = "https://files.pythonhosted.org/packages/4c/c4/13b4776ea2d76c115c1d1b84579f3764ee6d57204f6be27119f13a61d0a9/python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, +] "pyyaml 6.0" = [ {url = "https://files.pythonhosted.org/packages/02/25/6ba9f6bb50a3d4fbe22c1a02554dc670682a07c8701d1716d19ddea2c940/PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {url = "https://files.pythonhosted.org/packages/08/f4/ffa743f860f34a5e8c60abaaa686f82c9ac7a2b50e5a1c3b1eb564d59159/PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, @@ -633,6 +712,18 @@ content_hash = "sha256:f0267acbf584dfd2411f20fc3f63a5d42aa0bc85b37355aa528c0df8d {url = "https://files.pythonhosted.org/packages/3d/3b/4fd974fef00ad26d9b078700e932776126528eb384aaa1e20a0867af53e5/Quart-0.18.3.tar.gz", hash = "sha256:dc4de597d5d4693627c90904b233d729531f6b27c1164f760476d3967aee2a4a"}, {url = "https://files.pythonhosted.org/packages/5c/05/64e318eaf6dbeb8db79402cb71d6ecda676c9510f382b9243efe67b9ba50/Quart-0.18.3-py3-none-any.whl", hash = "sha256:c221a7deb83a014dee040108d654b6141fe37f59e249c5caa0fdcf6506caf50b"}, ] +"quart-auth 0.7.0" = [ + {url = "https://files.pythonhosted.org/packages/5b/50/d98d5ba030f87bc73e55973ad90e8ea61dbbdd252715e03b8395c5a374b3/quart_auth-0.7.0-py3-none-any.whl", hash = "sha256:7729fccded2d821179fb3005116672d129ce17aa3f248ad8991587132b323a35"}, + {url = "https://files.pythonhosted.org/packages/d6/9b/5da686100b031b549e1bc98019888cd1e95b8bee26d7efcd864e3fd537b1/quart-auth-0.7.0.tar.gz", hash = "sha256:e288f43789f694981e3695325cfe565361fdad252c5e1a663c5ca6ea41405a05"}, +] +"quart-rate-limiter 0.7.0" = [ + {url = "https://files.pythonhosted.org/packages/49/1d/c5567a0128e84dec57bf283eb9708b8e0c91ca80488e4652ae82b3015348/quart_rate_limiter-0.7.0-py3-none-any.whl", hash = "sha256:7d8d0a72051cf60368810364676c151128f77120274d60e8df4e9c94419b4ac4"}, + {url = "https://files.pythonhosted.org/packages/ab/90/55e48a9f0ce1628db103161351d65feeb15287b81ca570db66a4e6f4c0c5/quart-rate-limiter-0.7.0.tar.gz", hash = "sha256:e4cec1a0d476cf7fe2815e722d884e2cee8365bdd08f55480ed190ae07075a5d"}, +] +"six 1.16.0" = [ + {url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, +] "smmap 5.0.0" = [ {url = "https://files.pythonhosted.org/packages/21/2d/39c6c57032f786f1965022563eec60623bb3e1409ade6ad834ff703724f3/smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, {url = "https://files.pythonhosted.org/packages/6d/01/7caa71608bc29952ae09b0be63a539e50d2484bc37747797a66a60679856/smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9126c93..858738a 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -9,6 +9,10 @@ requires-python = ">=3.10" license = {text = "Private"} dependencies = [ "quart>=0.18.3", + "quart-auth>=0.7.0", + "bcrypt>=4.0.1", + "itsdangerous>=2.1.2", + "quart-rate-limiter>=0.7.0", ] [project.optional-dependencies] @@ -28,6 +32,7 @@ dev = [ "pytest>=7.1.2", "pytest-asyncio>=0.19.0", "djhtml>=1.5.2", + "freezegun>=1.2.2", ] [tool.black] diff --git a/backend/src/backend/blueprints/control.py b/backend/src/backend/blueprints/control.py index 953e569..1fb5fa0 100644 --- a/backend/src/backend/blueprints/control.py +++ b/backend/src/backend/blueprints/control.py @@ -1,9 +1,11 @@ from quart import Blueprint, ResponseReturnValue +from quart_rate_limiter import rate_exempt blueprint = Blueprint("control", __name__) @blueprint.get("/control/ping") +@rate_exempt async def ping() -> ResponseReturnValue: return {"ping": "pong"} diff --git a/backend/src/backend/run.py b/backend/src/backend/run.py index 3c28f5d..fea9cb7 100644 --- a/backend/src/backend/run.py +++ b/backend/src/backend/run.py @@ -1,16 +1,41 @@ from quart import Quart, ResponseReturnValue + +# Each blueprint is a logical collection of features in our web app from backend.blueprints.control import blueprint as control_blueprint +# For making sure error responses are in JSON format from backend.lib.api_error import APIError +# Rate limiting +from quart_rate_limiter import RateLimiter +from quart_rate_limiter import RateLimitExceeded +from datetime import timedelta -app = Quart(__name__) +# Authentication +from quart_auth import AuthManager + + +app: Quart = Quart(__name__) +auth_manager: AuthManager = AuthManager(app) +rate_limiter: RateLimiter = RateLimiter(app) + +# Configure the web app +# Either in DEV/DEBUG mode or TEST mode app.config.from_prefixed_env(prefix="TODO") app.register_blueprint(control_blueprint) +# rate limiting +@app.errorhandler(RateLimitExceeded) # type: ignore +async def handle_rate_limit_exceeded_error( + error: RateLimitExceeded, +) -> ResponseReturnValue: + return {}, error.get_headers(), 429 + + +# handles errors @app.errorhandler(APIError) # type: ignore async def handle_api_error(error: APIError) -> ResponseReturnValue: return {"code": error.code}, error.status_code diff --git a/backend/testing.env b/backend/testing.env index 0c95e99..03d9c74 100644 --- a/backend/testing.env +++ b/backend/testing.env @@ -2,3 +2,7 @@ TODO_BASE_URL="localhost:5050" TODO_DEBUG=true TODO_SECRET_KEY="secret key" TODO_TESTING=true + +# disable for when we not using HTTPS +TODO_QUART_AUTH_COOKIE_SECURE=false +TODO_QUART_AUTH_COOKIE_SAMESITE="Strict" diff --git a/backend/tests/test_rate_limits.py b/backend/tests/test_rate_limits.py new file mode 100644 index 0000000..312ef07 --- /dev/null +++ b/backend/tests/test_rate_limits.py @@ -0,0 +1,29 @@ +from quart_rate_limiter import ( + QUART_RATE_LIMITER_EXEMPT_ATTRIBUTE, + QUART_RATE_LIMITER_LIMITS_ATTRIBUTE, +) + +from backend.run import app + +IGNORED_ENDPOINTS = {"static"} + + +# Check if all api routes are rate limited +async def test_routes_have_rate_limits() -> None: + for rule in app.url_map.iter_rules(): + endpoint = rule.endpoint + + exempt = getattr( + app.view_functions[endpoint], + QUART_RATE_LIMITER_EXEMPT_ATTRIBUTE, + False, + ) + + if not exempt and endpoint not in IGNORED_ENDPOINTS: + rate_limits = getattr( + app.view_functions[endpoint], + QUART_RATE_LIMITER_LIMITS_ATTRIBUTE, + [], + ) + + assert rate_limits != []