Spaces:
Running
Running
large improve of tests and docker build
Browse files- .env.test +0 -0
- Dockerfile +45 -10
- pytest.ini +7 -0
- requirements-dev.txt +5 -0
- requirements.txt +0 -1
- tests/conftest.py +117 -4
- tests/repository/test_transaction_repo.py +22 -0
.env.test
ADDED
|
File without changes
|
Dockerfile
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
-
FROM python:3.11-slim
|
| 2 |
|
| 3 |
-
|
| 4 |
-
COPY ./entrypoint.sh /tmp/entrypoint.sh
|
| 5 |
-
COPY ./src /app/src
|
| 6 |
-
COPY ./tests /app/tests
|
| 7 |
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
# Avoid interactive prompts during package installation
|
| 11 |
ENV DEBIAN_FRONTEND=noninteractive
|
|
@@ -15,13 +15,47 @@ ENV DEBIAN_FRONTEND=noninteractive
|
|
| 15 |
# Clean up apt cache to reduce image size
|
| 16 |
# Use pip to install Python packages
|
| 17 |
RUN apt-get update && \
|
| 18 |
-
apt-get install -y --no-install-recommends
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 21 |
|
| 22 |
-
|
|
|
|
| 23 |
|
| 24 |
WORKDIR /app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
ENV PYTHONPATH=/app
|
| 27 |
|
|
@@ -34,7 +68,8 @@ CMD [ "curl", "-f", "http://localhost:8860/check_health" ]
|
|
| 34 |
|
| 35 |
# Create a non-root user 'appuser' and switch to this user
|
| 36 |
RUN useradd --create-home appuser
|
|
|
|
| 37 |
USER appuser
|
| 38 |
|
| 39 |
# CMD with JSON notation
|
| 40 |
-
CMD ["/
|
|
|
|
| 1 |
+
FROM python:3.11-slim-bookworm AS builder
|
| 2 |
|
| 3 |
+
WORKDIR /app
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
COPY requirements.txt requirements.txt
|
| 6 |
+
COPY requirements-dev.txt requirements-dev.txt
|
| 7 |
+
|
| 8 |
+
ARG TEST
|
| 9 |
|
| 10 |
# Avoid interactive prompts during package installation
|
| 11 |
ENV DEBIAN_FRONTEND=noninteractive
|
|
|
|
| 15 |
# Clean up apt cache to reduce image size
|
| 16 |
# Use pip to install Python packages
|
| 17 |
RUN apt-get update && \
|
| 18 |
+
apt-get install -y --no-install-recommends \
|
| 19 |
+
gcc \
|
| 20 |
+
curl && \
|
| 21 |
+
pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt && \
|
| 22 |
+
# Install development dependencies if TEST is true
|
| 23 |
+
if [ "$TEST" = "true" ]; then \
|
| 24 |
+
pip install --no-cache-dir -r requirements-dev.txt; \
|
| 25 |
+
fi && \
|
| 26 |
+
# Clean up apt cache and remove gcc and curl to reduce image size
|
| 27 |
+
apt-get remove -y gcc curl && \
|
| 28 |
+
apt-get autoremove -y && \
|
| 29 |
apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 30 |
|
| 31 |
+
# ---- Runtime stage ----
|
| 32 |
+
FROM python:3.11-slim-bookworm
|
| 33 |
|
| 34 |
WORKDIR /app
|
| 35 |
+
|
| 36 |
+
ARG TEST
|
| 37 |
+
|
| 38 |
+
COPY --from=builder /usr/local/lib/python3.11 /usr/local/lib/python3.11
|
| 39 |
+
COPY --from=builder /usr/local/bin /usr/local/bin
|
| 40 |
+
|
| 41 |
+
# For faking postgresql in tests
|
| 42 |
+
RUN if [ "$TEST" = "true" ]; then \
|
| 43 |
+
apt-get update && \
|
| 44 |
+
apt-get install -y --no-install-recommends \
|
| 45 |
+
postgresql \
|
| 46 |
+
postgresql-contrib && \
|
| 47 |
+
apt-get clean && rm -rf /var/lib/apt/lists/* \
|
| 48 |
+
else \
|
| 49 |
+
echo "Skipping PostgreSQL installation for non-test build"; \
|
| 50 |
+
fi
|
| 51 |
+
|
| 52 |
+
COPY ./entrypoint.sh /app/entrypoint.sh
|
| 53 |
+
COPY ./src /app/src
|
| 54 |
+
COPY ./tests /app/tests
|
| 55 |
+
COPY ./pytest.ini /app/pytest.ini
|
| 56 |
+
COPY ./.env.test /app/.env.test
|
| 57 |
+
|
| 58 |
+
RUN chmod +x /app/entrypoint.sh
|
| 59 |
|
| 60 |
ENV PYTHONPATH=/app
|
| 61 |
|
|
|
|
| 68 |
|
| 69 |
# Create a non-root user 'appuser' and switch to this user
|
| 70 |
RUN useradd --create-home appuser
|
| 71 |
+
RUN chown -R appuser:appuser /app
|
| 72 |
USER appuser
|
| 73 |
|
| 74 |
# CMD with JSON notation
|
| 75 |
+
CMD ["/app/entrypoint.sh"]
|
pytest.ini
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pytest.ini
|
| 2 |
+
[pytest]
|
| 3 |
+
env_files =
|
| 4 |
+
.env.test
|
| 5 |
+
|
| 6 |
+
filterwarnings =
|
| 7 |
+
ignore:.*Support for class-based `config` is deprecated.*:DeprecationWarning
|
requirements-dev.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This file is used to install the development dependencies for the project.
|
| 2 |
+
pytest==8.3.5
|
| 3 |
+
pytest-dotenv==0.5.2
|
| 4 |
+
pytest-postgresql==7.0.1
|
| 5 |
+
httpx==0.28.1
|
requirements.txt
CHANGED
|
@@ -3,5 +3,4 @@ fastapi==0.115.12
|
|
| 3 |
requests==2.32.3
|
| 4 |
sqlalchemy==2.0.40
|
| 5 |
psycopg[binary]==3.2.7
|
| 6 |
-
pytest==8.3.5
|
| 7 |
uvicorn==0.34.2
|
|
|
|
| 3 |
requests==2.32.3
|
| 4 |
sqlalchemy==2.0.40
|
| 5 |
psycopg[binary]==3.2.7
|
|
|
|
| 6 |
uvicorn==0.34.2
|
tests/conftest.py
CHANGED
|
@@ -1,8 +1,84 @@
|
|
|
|
|
| 1 |
import pytest
|
| 2 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from src.entity.transaction import Transaction
|
| 4 |
from src.entity.api.transaction_api import TransactionApi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
@pytest.fixture
|
| 7 |
def valid_transaction():
|
| 8 |
return Transaction(
|
|
@@ -21,7 +97,7 @@ def valid_transaction():
|
|
| 21 |
customer_dob='1968-03-19',
|
| 22 |
transaction_number='2da90c7d74bd46a0caf3777415b3ebd3',
|
| 23 |
transaction_amount=2.86,
|
| 24 |
-
transaction_datetime=
|
| 25 |
transaction_category='personal_care',
|
| 26 |
merchant_name='fraud_Kirlin and Sons',
|
| 27 |
merchant_address_latitude=33.986391,
|
|
@@ -46,7 +122,7 @@ def fraudulent_transaction():
|
|
| 46 |
customer_dob='1969-09-15',
|
| 47 |
transaction_number='16bf2e46c54369a8eab2214649506425',
|
| 48 |
transaction_amount=2400000.84,
|
| 49 |
-
transaction_datetime=
|
| 50 |
transaction_category='health_fitness',
|
| 51 |
merchant_name="fraud_Hamill-D'Amore",
|
| 52 |
merchant_address_latitude=32.575873,
|
|
@@ -76,4 +152,41 @@ def transaction_api():
|
|
| 76 |
customer_latitude=37.7749,
|
| 77 |
customer_longitude=-122.4194,
|
| 78 |
customer_city_population=870000,
|
| 79 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
import pytest
|
| 3 |
+
import pkgutil
|
| 4 |
+
import importlib
|
| 5 |
+
from fastapi.testclient import TestClient
|
| 6 |
+
from sqlalchemy import create_engine, text
|
| 7 |
+
from sqlalchemy.orm import sessionmaker
|
| 8 |
+
|
| 9 |
+
from src.entity.fraud_details import FraudDetails
|
| 10 |
from src.entity.transaction import Transaction
|
| 11 |
from src.entity.api.transaction_api import TransactionApi
|
| 12 |
+
from src.main import app, provide_connection
|
| 13 |
+
from src.repository.common import get_session
|
| 14 |
+
|
| 15 |
+
from src.entity import Base
|
| 16 |
+
|
| 17 |
+
# ----------------------------------------------------------------
|
| 18 |
+
# Load all classes from src.entity
|
| 19 |
+
module = importlib.import_module('src.entity')
|
| 20 |
+
|
| 21 |
+
# Loop over all modules in the 'src.entity' package
|
| 22 |
+
package = importlib.import_module('src.entity')
|
| 23 |
+
|
| 24 |
+
for _, module_name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
|
| 25 |
+
module = importlib.import_module(module_name)
|
| 26 |
+
|
| 27 |
+
@pytest.fixture
|
| 28 |
+
def client(db_session):
|
| 29 |
+
"""
|
| 30 |
+
Create a test client for the FastAPI app.
|
| 31 |
+
"""
|
| 32 |
+
# Override the get_session dependency to use the test database session
|
| 33 |
+
def override_get_session():
|
| 34 |
+
yield db_session
|
| 35 |
+
|
| 36 |
+
# Override the dependency in the FastAPI app
|
| 37 |
+
app.dependency_overrides[get_session] = override_get_session
|
| 38 |
+
app.dependency_overrides[provide_connection] = override_get_session
|
| 39 |
+
|
| 40 |
+
return TestClient(app)
|
| 41 |
+
|
| 42 |
+
# ----------------------------------------------------------------
|
| 43 |
+
# Fixtures for Database
|
| 44 |
+
# ----------------------------------------------------------------
|
| 45 |
+
@pytest.fixture
|
| 46 |
+
def db_session(postgresql):
|
| 47 |
+
"""
|
| 48 |
+
Create a new database session for each test
|
| 49 |
+
and tear it down after the test.
|
| 50 |
+
"""
|
| 51 |
+
# Create a new database connection
|
| 52 |
+
host = postgresql.info.host
|
| 53 |
+
port = postgresql.info.port
|
| 54 |
+
user = postgresql.info.user
|
| 55 |
+
dbname = postgresql.info.dbname
|
| 56 |
+
|
| 57 |
+
dsn = f"postgresql+psycopg://{user}@{host}:{port}/{dbname}"
|
| 58 |
+
engine = create_engine(dsn, echo=True)
|
| 59 |
+
|
| 60 |
+
# Create schema and tables once
|
| 61 |
+
with engine.begin() as conn:
|
| 62 |
+
conn.execute(text("CREATE SCHEMA IF NOT EXISTS public"))
|
| 63 |
+
|
| 64 |
+
Base.metadata.create_all(engine)
|
| 65 |
|
| 66 |
+
connection = engine.connect()
|
| 67 |
+
connection.begin()
|
| 68 |
+
|
| 69 |
+
SessionLocal = sessionmaker(bind=connection)
|
| 70 |
+
session = SessionLocal()
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
yield session
|
| 74 |
+
except Exception:
|
| 75 |
+
session.rollback() # In case of an error, rollback the session
|
| 76 |
+
raise
|
| 77 |
+
finally:
|
| 78 |
+
session.close()
|
| 79 |
+
connection.close()
|
| 80 |
+
|
| 81 |
+
# ----------------------------------------------------------------
|
| 82 |
@pytest.fixture
|
| 83 |
def valid_transaction():
|
| 84 |
return Transaction(
|
|
|
|
| 97 |
customer_dob='1968-03-19',
|
| 98 |
transaction_number='2da90c7d74bd46a0caf3777415b3ebd3',
|
| 99 |
transaction_amount=2.86,
|
| 100 |
+
transaction_datetime=datetime.datetime(2020, 6, 21, 12, 14, 25),
|
| 101 |
transaction_category='personal_care',
|
| 102 |
merchant_name='fraud_Kirlin and Sons',
|
| 103 |
merchant_address_latitude=33.986391,
|
|
|
|
| 122 |
customer_dob='1969-09-15',
|
| 123 |
transaction_number='16bf2e46c54369a8eab2214649506425',
|
| 124 |
transaction_amount=2400000.84,
|
| 125 |
+
transaction_datetime=datetime.datetime(2020, 6, 21, 22, 6, 39),
|
| 126 |
transaction_category='health_fitness',
|
| 127 |
merchant_name="fraud_Hamill-D'Amore",
|
| 128 |
merchant_address_latitude=32.575873,
|
|
|
|
| 152 |
customer_latitude=37.7749,
|
| 153 |
customer_longitude=-122.4194,
|
| 154 |
customer_city_population=870000,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
# ----------------------------------------------------------------
|
| 158 |
+
# Fixtures in database
|
| 159 |
+
# ----------------------------------------------------------------
|
| 160 |
+
@pytest.fixture
|
| 161 |
+
def valid_transaction_in_db(db_session, valid_transaction: Transaction):
|
| 162 |
+
"""
|
| 163 |
+
Create a valid transaction and add it to the database
|
| 164 |
+
"""
|
| 165 |
+
db_session.add(valid_transaction)
|
| 166 |
+
db_session.commit()
|
| 167 |
+
db_session.refresh(valid_transaction)
|
| 168 |
+
|
| 169 |
+
return valid_transaction
|
| 170 |
+
|
| 171 |
+
@pytest.fixture
|
| 172 |
+
def fraudulent_transaction_in_db(db_session, fraudulent_transaction: Transaction):
|
| 173 |
+
"""
|
| 174 |
+
Create a fraudulent transaction and add it to the database
|
| 175 |
+
"""
|
| 176 |
+
fraud_details = FraudDetails(
|
| 177 |
+
fraud_score=0.99,
|
| 178 |
+
model_version="v1.0",
|
| 179 |
+
notification_sent=False,
|
| 180 |
+
notification_recipients=None,
|
| 181 |
+
notification_datetime=None,
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
# Add the fraud details to the transaction
|
| 185 |
+
fraudulent_transaction.fraud_details = fraud_details
|
| 186 |
+
|
| 187 |
+
# Add the transaction to the database
|
| 188 |
+
db_session.add(fraudulent_transaction)
|
| 189 |
+
db_session.commit()
|
| 190 |
+
db_session.refresh(fraudulent_transaction)
|
| 191 |
+
|
| 192 |
+
return fraudulent_transaction
|
tests/repository/test_transaction_repo.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from src.entity.transaction import Transaction
|
| 2 |
+
from src.repository.transaction_repo import fetch_transaction
|
| 3 |
+
|
| 4 |
+
def test_fetch_transaction_valid(db_session, valid_transaction_in_db: Transaction):
|
| 5 |
+
"""
|
| 6 |
+
Test finding a transaction by its ID.
|
| 7 |
+
"""
|
| 8 |
+
id = valid_transaction_in_db.id
|
| 9 |
+
transaction = fetch_transaction(db_session, id)
|
| 10 |
+
assert transaction is not None
|
| 11 |
+
assert transaction.id == id
|
| 12 |
+
assert transaction.fraud_details is None, "Transaction should not have fraud details"
|
| 13 |
+
|
| 14 |
+
def test_fetch_transaction_fraudulent(db_session, fraudulent_transaction_in_db: Transaction):
|
| 15 |
+
"""
|
| 16 |
+
Test finding a transaction by its ID.
|
| 17 |
+
"""
|
| 18 |
+
id = fraudulent_transaction_in_db.id
|
| 19 |
+
transaction = fetch_transaction(db_session, id)
|
| 20 |
+
assert transaction is not None
|
| 21 |
+
assert transaction.id == id
|
| 22 |
+
assert transaction.fraud_details is not None, "Transaction should have fraud details"
|