SlimG commited on
Commit
5232fbd
·
1 Parent(s): 68536e4

large improve of tests and docker build

Browse files
.env.test ADDED
File without changes
Dockerfile CHANGED
@@ -1,11 +1,11 @@
1
- FROM python:3.11-slim
2
 
3
- COPY ./requirements.txt /tmp/requirements.txt
4
- COPY ./entrypoint.sh /tmp/entrypoint.sh
5
- COPY ./src /app/src
6
- COPY ./tests /app/tests
7
 
8
- RUN chmod +x /tmp/entrypoint.sh
 
 
 
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 gcc curl && \
19
- pip install --upgrade pip && \
 
 
 
 
 
 
 
 
 
20
  apt-get clean && rm -rf /var/lib/apt/lists/*
21
 
22
- RUN pip install --no-cache-dir -r /tmp/requirements.txt
 
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 ["/tmp/entrypoint.sh"]
 
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 pandas as pd
 
 
 
 
 
 
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=pd.to_datetime('2020-06-21 12:14:25'),
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=pd.to_datetime('2020-06-21 22:06:39'),
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"