From 6ed943ef200990369dcbb802c814035ffe765226 Mon Sep 17 00:00:00 2001 From: Kyattsukuro Date: Thu, 21 Aug 2025 11:30:49 +0200 Subject: [PATCH] tests --- .gitignore | 3 +- .vscode/launch.json | 3 + .vscode/settings.json | 5 +- README.md | 2 + data/data.sqlite | Bin 16384 -> 0 bytes data/db.sqlite | Bin 16384 -> 0 bytes docker/Dockerfile | 2 +- docker/default.sqlite | Bin 16384 -> 16384 bytes simple_chat_api/config.py | 11 +++- simple_chat_api/db_handler.py | 11 +++- simple_chat_api/requirements.txt | 5 ++ simple_chat_api/tests/auth_entpoints.py | 78 ++++++++++++++++++++++++ 12 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 README.md delete mode 100644 data/data.sqlite delete mode 100644 data/db.sqlite create mode 100644 simple_chat_api/tests/auth_entpoints.py diff --git a/.gitignore b/.gitignore index 46e5b6d..7932da9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .venv __pycache__ -*.pyc \ No newline at end of file +*.pyc +data.sqlite \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 54e899c..2dd9c05 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,6 +16,9 @@ "type": "debugpy", "request": "launch", "cwd": "${workspaceFolder}", + "env": { + "CREATE_OPTIONAL_DEFAULT_USER": "true" + }, "module": "simple_chat_api", "console": "integratedTerminal", "justMyCode": true diff --git a/.vscode/settings.json b/.vscode/settings.json index 372ffb7..f29e369 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,8 @@ }, "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", - "nixEnvSelector.nixFile": "${workspaceFolder}/shell.nix" + "nixEnvSelector.nixFile": "${workspaceFolder}/shell.nix", + "python.testing.unittestArgs": ["-s", "simple_chat_api/tests", "-p", "*.py"], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true } diff --git a/README.md b/README.md new file mode 100644 index 0000000..477da20 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +Run tests: +`python -m unittest discover -s simple_chat_api/tests -p "*.py"` diff --git a/data/data.sqlite b/data/data.sqlite deleted file mode 100644 index 299dd8b2cd436563128112140bb71c43702e71ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI($!^*}7zc2hB@0#JN>SxQ6?Gy-Y9du`uz?U&l|qOC7tCUogj}Kl3^;fJ+iblh z*S<&l06q2@daQbDk3II-9y@kIm8fcOm0IZ=VLp!y{~7&!nn6@61t(E{Y;cAs9{_(NTxxU0i8<~tQ!e?u`9<~=FUKmY_l00ck)1V8`; zKmY_l00cnbcLJA>gUjsZCVjQ#;+C!&s^j3E>MUv~t%#|bh-#^$f`}GZ(N1Jl>maFE z6SJa%ilrJV)(eFvD6(o>mVp{6C7nwtUcYUbu4=lsFO415zTI~|>|DxN$vW{pSaN z8QDuAWA6r**yk)wX=X>AIU}8{?9cJowfz3MKjwMVFF^wBt?#Kda)j4kQOsLLV;-%4 zAg9nk00ck)1V8`;KmY_l00ck)1VG?#3gp7!jmz+ZvEJS0Tia27`&39iHMsaBT60Hc zOgIqaS~opb(n|fbkeuvwXZ_v#WTWDrs+dn+y3#nu%|c&Ia-9iBnHV z-B<^o&58xt&RQH-7LPKeNj$z^GL?p`OeJYj8@-5CE4gHnzfKDJNdwKeN$MXo^1Pjh zT4lMHof`FOXPR%Qt*Jaa<)X26!W6~Hs4=c}8n69YZs3 zvZQw1j!*Z8QU;&mY$H>S&l?S{#W_;*;W)AFJor~s-4E6&e(|~@hXMfzKmY;|fB*y_009U<00I#BOJK2I+V&d_XSpZY-IQls2=>T@ z?aQ4w43dy0!D$pyJ4>5hB^}dlFA2}WnD(v`+PjIO1L{>~MUm0lAnu$8u^!(Sx#YQA zotg_iTOG?(#g#03vH|sT*KzkUh==qd9MWbwZh5U&XWRFEXE88|>L9{2?plgwO6Qle zjN4V#GMq3mu@}BFo_^o>Yh@ipt=)3B{1e|HX+GvJ;(4l0_9Hfzg&B{`nxiArl@#=j z#;)q7t{W5xKmY;|fB*y_009U<00Izz00cHwz|$MU`F~SiFZKli2tWV=5P$##AOHaf jKmY;|Far4hj}HL?5P$##AOHafKmY;|fB*y_u=xU?bA4<; diff --git a/docker/Dockerfile b/docker/Dockerfile index efdc014..4b0e88a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -25,7 +25,7 @@ RUN rm /etc/nginx/nginx.conf RUN ln -s /simple-chat/docker/nginx.conf /etc/nginx/nginx.conf RUN mkdir -p /var/nginx - +COPY docker/default.sqlite /simple-chat/data/data.sqlite # Dev stage FROM base AS dev diff --git a/docker/default.sqlite b/docker/default.sqlite index d2f45abed3e140cb1bc390b9e41780e7b3623a5d..299dd8b2cd436563128112140bb71c43702e71ca 100644 GIT binary patch delta 485 zcmXZYy>pXr7zc1@f<`_0+3R^7>Rs_J=STx2NY5$KG+;_nLde@x8h*SOUIG*XQ{&u4 z&ws&l^?DgQ`zLtabaZrdm0+gt^n9N0^Q>0rYK4A0Kr5eK@Mz`s;tRkr{00GQV1q;a z2Y$2OeU{uK*5^8W2cy;EOkp>1I_xZj$KI4*_ky_?^ zT4V)AZA(qZpU&vH%*PQ7g>oU;p0<^y^G16VsjO(!b#0n<&!n*JljM2p?P)J*G+y_- z{_tArFGVpK1n=sKEN~qCpDGp9+;+E=bv*3QAxrt^lG$Fms-iDDLwLEA(lJTZXROz1 zCBZOO?b%{7f!Ro&D}Gj#+9@PgOScjyQ%;A8PH9(bFhTN4?w&ACk6Z*tG$&h5t(ner zG3kii&}&Y-OwwvfX0V^X9jZ76?h)`8`~tthA8^0kCB6gZVQCM|e}`={zr4K3ymo1m z`O=+AY5$R?ySDcfK@b$&A>b|_yag>DeSjR8`(+b)Fw`6YI*w0_{Ls-~K2~gZ;bg>B cwtz(D#~~rXDAG(wSTKado09^xzW#jj5Avswz5oCK delta 1125 zcmaKrzfTlF6vyul5s+QBP;y=t^(mNGm{UmfurSexcu|5##2*yKVfP(3$?VLsGlM9_ zK`czHFj{Cub0K!dnAljD7`5|%u&}c-7Ha2P_U?#{*=BdY?|r`S`(}4-Ex)#w-|+L5 zCqozVmDhKho>TBJ=e_aJD-L{la~S5l%TtAJf4zH9s|c0Ek+Fr5(Xo-G>$9h;4^B?k z>NRs?rfM#S^Rp+e-Zp1qb8UQfp?ry*s#UH`&SN+~Gac0DS7xg-_UWrbb7b&6=l$}& zrxzK#^`2}TJ=@>q=UWDa-mYRF3nMPwTrTH1GUt8u-nn1h5AJh!)xG1Ma}T=(=bQ81 zX*!P_Ik5lr`lQn+db@MJSKPIT5)&rIwh8sE&t3ii3B`_3MIls6{Perzrd@u!YqT{( zee2`uutU;tN80X#n<^3EPLV&&c@dHhqtshP`whdi{>fHff40^tEohDM0k zqGZ4XUusxZhB*T$c|>ra&xdZ7>5)apVaak0QfZUe5O4*PfE#8wMYck)C{P(XE#jJ| zRw>i+n2G@zO&-xuDoe0rnJL3Wie%Ox@WdwAXr<*65{wh9qYN>j==5S)2^*6i(ma$E zP-}#M3(?*YMY2${i<}vd=|m8-gdS{ChKnl!#s+C+n$s3k5HJj=F0e5V8Br;hgt(C? z9->SanJ$Z511n;dCI?lk@qjZ3QDc&-krL?~EL{<&h3M95ZB#z8bG!q?1v>oy{s9}! VX9x3CRPsyhUnu*SI+VvR{sJRJeM|rV diff --git a/simple_chat_api/config.py b/simple_chat_api/config.py index b0a8547..3d70061 100644 --- a/simple_chat_api/config.py +++ b/simple_chat_api/config.py @@ -1,5 +1,14 @@ from passlib.context import CryptContext +import os JWT_SECRET = "F&M2eb%*T2dnhZqxw^ts6qotqF&M2eb%*T2dnhZqxw^ts6qotq" -hash_context = CryptContext(schemes=["bcrypt"]) \ No newline at end of file +hash_context = CryptContext(schemes=["bcrypt"]) +db_url = os.environ.get("DATABASE_URL", "sqlite:///./data/data.sqlite") +use_optional_default_user = os.environ.get("CREATE_OPTIONAL_DEFAULT_USER", "False").lower() in ("true", "1", "t") +optional_default_user = [ + {"name": "max", "hash": "$2b$12$8Q1lK3sF2ma53qvQND3lO.pq/28Qhl0AxcdIvKINrnAYnyMa0Syf6", "role": "user"}, + {"name": "kim", "hash": "$2b$12$h9VK2r61oPMgGwmUSdwKVebwMxX.14c6nEEvqVuUicpYYeyWQkSoy", "role": "user"}, + {"name": "ina", "hash": "$2b$12$Zk5GWsU6If4daZxNLMrGo..PEBFOv557OnRVMRwIIvTqJ4SQH882C", "role": "user"}, + {"name": "ulf", "hash": "$2b$12$X38/m.5v1Ttqn4393MTfCuRCRUXL8v0fxhgj2I6H3UGMAloZc2kQC", "role": "user"} + ] \ No newline at end of file diff --git a/simple_chat_api/db_handler.py b/simple_chat_api/db_handler.py index f72fa61..a8d8c2b 100644 --- a/simple_chat_api/db_handler.py +++ b/simple_chat_api/db_handler.py @@ -5,6 +5,8 @@ from dataclasses import dataclass import time import regex +from simple_chat_api.config import optional_default_user, use_optional_default_user + Base = declarative_base() @@ -32,11 +34,18 @@ class DbConnector: self.session = sessionmaker(bind=self.engine)() self._create_defaults() + + def _create_optional_default_user(self): + for user in optional_default_user: + self.add_user(name=user["name"], hash=user["hash"], role=user["role"]) + def _create_defaults(self): try: self.add_user(name="admin", hash="$2b$12$IcUr5w7pIFaXaGVFP5yVV.b.sIYjDbETR3l2PKgWO4nkrHU.1HmFa", role="admin") + if use_optional_default_user: + self._create_optional_default_user() except ValueError as e: - print(f"Default admin user already exists: {e}") + print(f"Default already exists: {e}") def get_user(self, name: str) -> User | dict[User] | None: if not name: diff --git a/simple_chat_api/requirements.txt b/simple_chat_api/requirements.txt index 4d013bf..c6c5f7f 100644 --- a/simple_chat_api/requirements.txt +++ b/simple_chat_api/requirements.txt @@ -1,11 +1,16 @@ bcrypt==4.3.0 bottle==0.13.4 +certifi==2025.8.3 cffi==1.17.1 +charset-normalizer==3.4.3 cryptography==45.0.4 greenlet==3.2.3 +idna==3.10 passlib==1.7.4 pycparser==2.22 PyJWT==2.10.1 regex==2025.7.34 +requests==2.32.5 SQLAlchemy==2.0.41 typing_extensions==4.14.0 +urllib3==2.5.0 diff --git a/simple_chat_api/tests/auth_entpoints.py b/simple_chat_api/tests/auth_entpoints.py new file mode 100644 index 0000000..b160a4a --- /dev/null +++ b/simple_chat_api/tests/auth_entpoints.py @@ -0,0 +1,78 @@ +import unittest +import threading +import subprocess +import time +import os +import signal +import requests + +server_process = None +API_URL = "http://localhost:7000/api" +def setUpModule(): + """Start the API server in a separate process before running tests""" + def run_server(): + global server_process + # Needet to get python3 dir + env = os.environ.copy() + env.update({ + "DATABASE_URL": "sqlite:///./data/TEST.sqlite", + "CREATE_OPTIONAL_DEFAULT_USER": "true" + }) + + server_process = subprocess.Popen( + ["python3", "-m", "simple_chat_api"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env + ) + + # Start server in a separate thread + server_thread = threading.Thread(target=run_server) + server_thread.daemon = True + server_thread.start() + + # Wait for server to start + time.sleep(2) + +def tearDownModule(): + global server_process + """Kill the API server after all tests have executed""" + if server_process: + server_process.send_signal(signal.SIGTERM) + server_process.wait() + +class TestServer(unittest.TestCase): + + def test_online(self): + """Test if the server is running""" + response = requests.get(API_URL) + self.assertEqual(response.status_code, 404, "Server is not running or not reachable") + + +class TestAuthEndpoints(unittest.TestCase): + def __init__(self, methodName = "runTest"): + super().__init__(methodName) + self.users = { + "admin": "admin", + "max": "12345" + } + self.userSessions = {} + + def test_get_token(self): + """Test the /token endpoint""" + for user,password in self.users.items(): + with self.subTest(user=user): + self.userSessions[user] = requests.Session() + response = self.userSessions[user].post(f"{API_URL}/user/token", json={ + "user": user, + "password": password + }) + self.assertEqual(response.status_code, 200, f"Failed to get token for user {user}; {response.text}") + data = response.json() + excepted = { + "sub": { + "user": user, + "role": "admin" if user == "admin" else "user" + }, + } + self.assertEqual(data["sub"], excepted["sub"], f"Token content mismatch for user {user}")