diff --git a/README.md b/README.md index 5dfdec6..aec89f5 100644 --- a/README.md +++ b/README.md @@ -267,16 +267,6 @@ DEFAULT_RATE_LIMIT_LIMIT=10 # default=10 DEFAULT_RATE_LIMIT_PERIOD=3600 # default=3600 ``` -For tests (optional to run): - -``` -# ------------- test ------------- -TEST_NAME="Tester User" -TEST_EMAIL="test@tester.com" -TEST_USERNAME="testeruser" -TEST_PASSWORD="Str1ng$t" -``` - And Finally the environment: ``` @@ -553,9 +543,11 @@ First, you may want to take a look at the project structure and understand what ├── LICENSE.md # License file for the project. │ ├── tests # Unit and integration tests for the application. +│ ├──helpers # Helper functions for tests. +│ │ ├── generators.py # Helper functions for generating test data. +│ │ └── mocks.py # Mock function for testing. │ ├── __init__.py │ ├── conftest.py # Configuration and fixtures for pytest. -│ ├── helper.py # Helper functions for tests. │ └── test_user.py # Test cases for user-related functionality. │ └── src # Source code directory. @@ -1842,16 +1834,6 @@ And finally, on your browser: `http://localhost/docs`. ## 7. Testing -For tests, ensure you have in `.env`: - -``` -# ------------- test ------------- -TEST_NAME="Tester User" -TEST_EMAIL="test@tester.com" -TEST_USERNAME="testeruser" -TEST_PASSWORD="Str1ng$t" -``` - While in the tests folder, create your test file with the name "test\_{entity}.py", replacing entity with what you're testing ```sh @@ -1876,7 +1858,6 @@ First you need to uncomment the following part in the `docker-compose.yml` file: # - ./src/.env # depends_on: # - db - # - create_superuser # - redis # command: python -m pytest ./tests # volumes: @@ -1895,7 +1876,6 @@ You'll get: - ./src/.env depends_on: - db - - create_superuser - redis command: python -m pytest ./tests volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 70ae366..6dabb8f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -102,7 +102,6 @@ services: # - ./src/.env # depends_on: # - db - # - create_superuser # - redis # command: python -m pytest ./tests # volumes: diff --git a/pyproject.toml b/pyproject.toml index 64547b1..5e6ed17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,9 @@ arq = "^0.25.0" gunicorn = "^22.0.0" bcrypt = "^4.1.1" fastcrud = "^0.12.0" +faker = "^26.0.0" +psycopg2-binary = "^2.9.9" +pytest-mock = "^3.14.0" [build-system] diff --git a/src/app/core/config.py b/src/app/core/config.py index e8b85bf..4730131 100644 --- a/src/app/core/config.py +++ b/src/app/core/config.py @@ -67,10 +67,7 @@ class FirstUserSettings(BaseSettings): class TestSettings(BaseSettings): - TEST_NAME: str = config("TEST_NAME", default="Tester User") - TEST_EMAIL: str = config("TEST_EMAIL", default="test@tester.com") - TEST_USERNAME: str = config("TEST_USERNAME", default="testeruser") - TEST_PASSWORD: str = config("TEST_PASSWORD", default="Str1ng$t") + ... class RedisCacheSettings(BaseSettings): diff --git a/tests/conftest.py b/tests/conftest.py index 4c6b61e..28002e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,39 @@ +from typing import Any, Callable, Generator + import pytest +from faker import Faker from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm.session import Session +from src.app.core.config import settings from src.app.main import app +DATABASE_URI = settings.POSTGRES_URI +DATABASE_PREFIX = settings.POSTGRES_SYNC_PREFIX + +sync_engine = create_engine(DATABASE_PREFIX + DATABASE_URI) +local_session = sessionmaker(autocommit=False, autoflush=False, bind=sync_engine) + + +fake = Faker() + @pytest.fixture(scope="session") -def client(): +def client() -> Generator[TestClient, Any, None]: with TestClient(app) as _client: yield _client + app.dependency_overrides = {} + sync_engine.dispose() + + +@pytest.fixture +def db() -> Generator[Session, Any, None]: + session = local_session() + yield session + session.close() + + +def override_dependency(dependency: Callable[..., Any], mocked_response: Any) -> None: + app.dependency_overrides[dependency] = lambda: mocked_response diff --git a/tests/helper.py b/tests/helper.py deleted file mode 100644 index 2163593..0000000 --- a/tests/helper.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi.testclient import TestClient - - -def _get_token(username: str, password: str, client: TestClient): - return client.post( - "/api/v1/login", - data={"username": username, "password": password}, - headers={"content-type": "application/x-www-form-urlencoded"}, - ) diff --git a/tests/helpers/generators.py b/tests/helpers/generators.py new file mode 100644 index 0000000..3910342 --- /dev/null +++ b/tests/helpers/generators.py @@ -0,0 +1,26 @@ +import uuid as uuid_pkg + +from sqlalchemy.orm import Session + +from src.app import models +from src.app.core.security import get_password_hash +from tests.conftest import fake + + +def create_user(db: Session, is_super_user: bool = False) -> models.User: + _user = models.User( + name=fake.name(), + username=fake.user_name(), + email=fake.email(), + hashed_password=get_password_hash(fake.password()), + profile_image_url=fake.image_url(), + uuid=uuid_pkg.uuid4(), + is_superuser=is_super_user, + ) + + db.add(_user) + db.commit() + db.refresh(_user) + + return _user + diff --git a/tests/helpers/mocks.py b/tests/helpers/mocks.py new file mode 100644 index 0000000..713ae68 --- /dev/null +++ b/tests/helpers/mocks.py @@ -0,0 +1,17 @@ +from typing import Any + +from fastapi.encoders import jsonable_encoder + +from src.app import models +from tests.conftest import fake + + +def get_current_user(user: models.User) -> dict[str, Any]: + return jsonable_encoder(user) + + +def oauth2_scheme() -> str: + token = fake.sha256() + if isinstance(token, bytes): + token = token.decode("utf-8") + return token # type: ignore diff --git a/tests/test_user.py b/tests/test_user.py index 954829f..ab12b0d 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -1,63 +1,81 @@ +from fastapi import status from fastapi.testclient import TestClient +from pytest_mock import MockerFixture +from sqlalchemy.orm import Session -from src.app.core.config import settings -from src.app.main import app +from src.app.api.dependencies import get_current_user +from src.app.api.v1.users import oauth2_scheme +from tests.conftest import fake, override_dependency -from .helper import _get_token - -test_name = settings.TEST_NAME -test_username = settings.TEST_USERNAME -test_email = settings.TEST_EMAIL -test_password = settings.TEST_PASSWORD - -admin_username = settings.ADMIN_USERNAME -admin_password = settings.ADMIN_PASSWORD - -client = TestClient(app) +from .helpers import generators, mocks def test_post_user(client: TestClient) -> None: response = client.post( "/api/v1/user", - json={"name": test_name, "username": test_username, "email": test_email, "password": test_password}, + json={ + "name": fake.name(), + "username": fake.user_name(), + "email": fake.email(), + "password": fake.password(), + }, ) - assert response.status_code == 201 + assert response.status_code == status.HTTP_201_CREATED + +def test_get_user(db: Session, client: TestClient) -> None: + user = generators.create_user(db) -def test_get_user(client: TestClient) -> None: - response = client.get(f"/api/v1/user/{test_username}") - assert response.status_code == 200 + response = client.get(f"/api/v1/user/{user.username}") + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + + assert response_data["id"] == user.id + assert response_data["username"] == user.username + + +def test_get_multiple_users(db: Session, client: TestClient) -> None: + for _ in range(5): + generators.create_user(db) -def test_get_multiple_users(client: TestClient) -> None: response = client.get("/api/v1/users") - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK + response_data = response.json()["data"] + assert len(response_data) >= 5 -def test_update_user(client: TestClient) -> None: - token = _get_token(username=test_username, password=test_password, client=client) - response = client.patch( - f"/api/v1/user/{test_username}", - json={"name": f"Updated {test_name}"}, - headers={"Authorization": f'Bearer {token.json()["access_token"]}'}, - ) - assert response.status_code == 200 +def test_update_user(db: Session, client: TestClient) -> None: + user = generators.create_user(db) + new_name = fake.name() + override_dependency(get_current_user, mocks.get_current_user(user)) -def test_delete_user(client: TestClient) -> None: - token = _get_token(username=test_username, password=test_password, client=client) + response = client.patch(f"/api/v1/user/{user.username}", json={"name": new_name}) + assert response.status_code == status.HTTP_200_OK - response = client.delete( - f"/api/v1/user/{test_username}", headers={"Authorization": f'Bearer {token.json()["access_token"]}'} - ) - assert response.status_code == 200 +def test_delete_user(db: Session, client: TestClient, mocker: MockerFixture) -> None: + user = generators.create_user(db) -def test_delete_db_user(client: TestClient) -> None: - token = _get_token(username=admin_username, password=admin_password, client=client) + override_dependency(get_current_user, mocks.get_current_user(user)) + override_dependency(oauth2_scheme, mocks.oauth2_scheme()) - response = client.delete( - f"/api/v1/db_user/{test_username}", headers={"Authorization": f'Bearer {token.json()["access_token"]}'} - ) - assert response.status_code == 200 + mocker.patch("src.app.core.security.jwt.decode", return_value={"sub": user.username, "exp": 9999999999}) + + response = client.delete(f"/api/v1/user/{user.username}") + assert response.status_code == status.HTTP_200_OK + + +def test_delete_db_user(db: Session, mocker: MockerFixture, client: TestClient) -> None: + user = generators.create_user(db) + super_user = generators.create_user(db, is_super_user=True) + + override_dependency(get_current_user, mocks.get_current_user(super_user)) + override_dependency(oauth2_scheme, mocks.oauth2_scheme()) + + mocker.patch("src.app.core.security.jwt.decode", return_value={"sub": user.username, "exp": 9999999999}) + + response = client.delete(f"/api/v1/db_user/{user.username}") + assert response.status_code == status.HTTP_200_OK