Skip to content

Commit e6689af

Browse files
Merge PR #772 - configure CI and pytest workflows
2 parents 94c879b + 064a4ff commit e6689af

3 files changed

Lines changed: 186 additions & 1 deletion

File tree

‎.github/workflows/ci.yml‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: CI – Run Tests
2+
3+
on:
4+
push:
5+
branches: ["main", "master"]
6+
pull_request:
7+
branches: ["main", "master"]
8+
9+
jobs:
10+
test:
11+
name: pytest
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.11"
20+
cache: "pip"
21+
22+
- name: Install dependencies
23+
run: |
24+
pip install --upgrade pip
25+
pip install -r backend/requirements.txt
26+
pip install pytest pytest-cov
27+
pip install flask-wtf bleach Pillow beautifulsoup4 "pydantic[email]" pytest pytest-cov
28+
29+
- name: Run pytest
30+
env:
31+
GOOGLE_BOOKS_API_KEY: "ci-fake-key"
32+
OPENAI_API_KEY: "ci-fake-key"
33+
GROQ_API_KEY: "ci-fake-key"
34+
GEMINI_API_KEY: "ci-fake-key"
35+
FLASK_ENV: testing
36+
PYTHONPATH: ".:backend"
37+
run: |
38+
python -m pytest tests/ \
39+
--ignore=tests/test_llm.py \
40+
--ignore=tests/test_mood_improvements.py \
41+
--ignore=tests/test_mood_cache_db.py \
42+
--ignore=tests/test_category_cache.py \
43+
--ignore=tests/test_env_validation.py \
44+
--ignore=tests/test_purchase_links.py \
45+
-v --tb=short --cov=backend
46+
47+
- name: Install dependencies
48+
run: |
49+
pip install --upgrade pip
50+
pip install torch --index-url https://download.pytorch.org/whl/cpu
51+
pip install -r backend/requirements.txt
52+
pip install flask-wtf bleach Pillow pytest pytest-cov
53+
54+
- name: Install dependencies
55+
run: |
56+
pip install --upgrade pip
57+
pip install torch --index-url https://download.pytorch.org/whl/cpu
58+
pip install -r backend/requirements.txt
59+
pip install flask-wtf bleach Pillow beautifulsoup4 pytest pytest-cov

‎backend/config.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ def from_env(cls) -> 'GoogleOAuthConfig':
166166
redirect_uri=os.getenv('GOOGLE_OAUTH_REDIRECT_URI'),
167167
frontend_redirect_url=os.getenv('FRONTEND_URL', 'http://127.0.0.1:5500/frontend/pages/library.html'),
168168
scope=os.getenv('GOOGLE_OAUTH_SCOPE', 'openid email profile')
169-
169+
)
170+
171+
@dataclass
170172
class EmailConfig:
171173
"""Email service configuration (e.g., SendGrid, Mailgun)."""
172174
api_key: Optional[str]

‎tests/conftest.py‎

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""
2+
tests/conftest.py
3+
Shared pytest fixtures. No live server needed — uses Flask test client
4+
with an in-memory SQLite database.
5+
"""
6+
7+
import sys
8+
import os
9+
from unittest.mock import MagicMock, patch
10+
11+
import pytest
12+
13+
# Make backend/ importable
14+
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
15+
BACKEND = os.path.join(ROOT, "backend")
16+
for p in (ROOT, BACKEND):
17+
if p not in sys.path:
18+
sys.path.insert(0, p)
19+
20+
# Stub out heavy optional packages so tests don't need GPU/model downloads
21+
for _mod in [
22+
"transformers", "sentence_transformers",
23+
"nltk", "nltk.tokenize", "nltk.tokenize.api",
24+
"huggingface_hub", "safetensors", "tokenizers", "tqdm", "joblib",
25+
"textblob", "textblob.blob", "textblob.base",
26+
]:
27+
sys.modules.setdefault(_mod, MagicMock())
28+
29+
# Set required env vars before importing app so validation passes
30+
os.environ.setdefault("GOOGLE_BOOKS_API_KEY", "fake-key-for-testing")
31+
os.environ.setdefault("SECRET_KEY", "test-secret-key")
32+
os.environ.setdefault("JWT_SECRET_KEY", "test-jwt-secret-key")
33+
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
34+
os.environ.setdefault("OPENAI_API_KEY", "fake-openai-key")
35+
os.environ.setdefault("GROQ_API_KEY", "fake-groq-key")
36+
os.environ.setdefault("GEMINI_API_KEY", "fake-gemini-key")
37+
os.environ.setdefault("FLASK_ENV", "testing")
38+
39+
from backend.app import app as flask_app
40+
from backend.models import db as _db, User
41+
42+
43+
@pytest.fixture(scope="session")
44+
def app():
45+
flask_app.config.update(
46+
TESTING=True,
47+
SQLALCHEMY_DATABASE_URI="sqlite:///:memory:",
48+
SQLALCHEMY_TRACK_MODIFICATIONS=False,
49+
JWT_SECRET_KEY="test-secret-key",
50+
SECRET_KEY="test-secret-key",
51+
RATELIMIT_ENABLED=False,
52+
)
53+
with flask_app.app_context():
54+
_db.create_all()
55+
yield flask_app
56+
_db.drop_all()
57+
58+
59+
@pytest.fixture
60+
def client(app):
61+
with app.test_client() as c:
62+
with app.app_context():
63+
yield c
64+
_db.session.rollback()
65+
for tbl in reversed(_db.metadata.sorted_tables):
66+
_db.session.execute(tbl.delete())
67+
_db.session.commit()
68+
69+
70+
@pytest.fixture
71+
def test_user(app):
72+
with app.app_context():
73+
user = User(username="testuser", email="test@example.com")
74+
user.set_password("Password123!")
75+
_db.session.add(user)
76+
_db.session.commit()
77+
_db.session.refresh(user)
78+
return user
79+
80+
81+
@pytest.fixture
82+
def auth_headers(client, test_user):
83+
resp = client.post(
84+
"/api/auth/login",
85+
json={"username": "testuser", "password": "Password123!"},
86+
)
87+
token = resp.get_json().get("access_token", "")
88+
return {"Authorization": f"Bearer {token}"}
89+
90+
91+
# --- Mocks for external services (issue #511 requirement) ---
92+
93+
@pytest.fixture
94+
def mock_openai():
95+
with patch("backend.ai_service.openai") as m:
96+
m.ChatCompletion.create.return_value = MagicMock(
97+
choices=[MagicMock(message=MagicMock(content="Mocked AI response"))]
98+
)
99+
yield m
100+
101+
102+
@pytest.fixture
103+
def mock_groq():
104+
with patch("backend.ai_service.groq") as m:
105+
m.chat.completions.create.return_value = MagicMock(
106+
choices=[MagicMock(message=MagicMock(content="Mocked Groq response"))]
107+
)
108+
yield m
109+
110+
111+
@pytest.fixture
112+
def mock_gemini():
113+
with patch("backend.ai_service.genai") as m:
114+
m.GenerativeModel.return_value.generate_content.return_value = MagicMock(
115+
text="Mocked Gemini response"
116+
)
117+
yield m
118+
119+
120+
@pytest.fixture
121+
def mock_price_tracker():
122+
with patch("backend.price_tracker.price_tracker.PriceTracker.fetch_price") as m:
123+
m.return_value = {"price": 9.99, "currency": "USD", "source": "mock"}
124+
yield m

0 commit comments

Comments
 (0)