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 299dd8b..0000000 Binary files a/data/data.sqlite and /dev/null differ diff --git a/data/db.sqlite b/data/db.sqlite deleted file mode 100644 index 6e85316..0000000 Binary files a/data/db.sqlite and /dev/null differ 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 d2f45ab..299dd8b 100644 Binary files a/docker/default.sqlite and b/docker/default.sqlite differ 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}")