Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 3 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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="[email protected]"
TEST_USERNAME="testeruser"
TEST_PASSWORD="Str1ng$t"
```

And Finally the environment:

```
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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="[email protected]"
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
Expand All @@ -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:
Expand All @@ -1895,7 +1876,6 @@ You'll get:
- ./src/.env
depends_on:
- db
- create_superuser
- redis
command: python -m pytest ./tests
volumes:
Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ services:
# - ./src/.env
# depends_on:
# - db
# - create_superuser
# - redis
# command: python -m pytest ./tests
# volumes:
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
5 changes: 1 addition & 4 deletions src/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="[email protected]")
TEST_USERNAME: str = config("TEST_USERNAME", default="testeruser")
TEST_PASSWORD: str = config("TEST_PASSWORD", default="Str1ng$t")
...


class RedisCacheSettings(BaseSettings):
Expand Down
31 changes: 30 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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
9 changes: 0 additions & 9 deletions tests/helper.py

This file was deleted.

26 changes: 26 additions & 0 deletions tests/helpers/generators.py
Original file line number Diff line number Diff line change
@@ -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

17 changes: 17 additions & 0 deletions tests/helpers/mocks.py
Original file line number Diff line number Diff line change
@@ -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
98 changes: 58 additions & 40 deletions tests/test_user.py
Original file line number Diff line number Diff line change
@@ -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