Skip to content
Open
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
8 changes: 8 additions & 0 deletions .github/workflows/E2E.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ jobs:
runs-on: ubuntu-latest
env:
SPOONACULAR_API_KEY: ${{ secrets.SPOONACULAR_API_KEY }}
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL}}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY}}

steps:
- name: Checkout code
Expand Down Expand Up @@ -62,3 +64,9 @@ jobs:

- name: Run Cypress tests
run: cd frontend && npm run cy:run

- name: Upload Cypress screenshots
uses: actions/upload-artifact@v4
with:
name: cypress-screenshots
path: frontend/cypress/screenshots
148 changes: 148 additions & 0 deletions .github/workflows/arihant.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: All Combined Tests

on:
pull_request:
branches:
- main

jobs:
# Backend tests with pytest (FastAPI)
test-backend:
runs-on: ubuntu-latest

env:
SPOONACULAR_API_KEY: ${{ secrets.SPOONACULAR_API_KEY }}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"

- name: Install backend dependencies
run: |
cd backend
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install pytest

- name: Debug Environment
run: |
cd backend
source venv/bin/activate
echo "API Key presence check:"
echo "$SPOONACULAR_API_KEY"
echo "Python environment:"
which python
python --version

- name: Run pytest tests
run: |
cd backend
source venv/bin/activate
export SPOONACULAR_API_KEY=${{ secrets.SPOONACULAR_API_KEY }}
python -m pytest -v

# Frontend tests with Vitest
test-frontend:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Clean install dependencies
run: |
cd frontend
rm -rf node_modules package-lock.json
npm install
npm install @rollup/rollup-linux-x64-gnu

- name: Create .env file
run: |
cd frontend
echo "VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}" >> .env
echo "VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}" >> .env

- name: Run Vitest tests
run: |
cd frontend
npm run test -- --run

# E2E Tests
e2e:
runs-on: ubuntu-latest
env:
SPOONACULAR_API_KEY: ${{ secrets.SPOONACULAR_API_KEY }}
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: "20"

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"

- name: Clean install dependencies
run: |
cd frontend
rm -rf node_modules package-lock.json
npm install
npm install @rollup/rollup-linux-x64-gnu

- name: Install backend dependencies
run: |
cd backend
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

- name: Create .env file for frontend
run: |
cd frontend
echo "VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}" >> .env
echo "VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}" >> .env

- name: Build the project
run: cd frontend && npm run build

- name: Start preview server
run: cd frontend && npm run preview &

- name: Start backend server
run: |
cd backend
source venv/bin/activate
python3 -m fastapi dev main.py &

- name: Wait for server to be ready
run: npx wait-on http://localhost:4173

- name: Run Cypress tests
run: cd frontend && npm run cy:run
continue-on-error: true

- name: Upload Cypress screenshots
uses: actions/upload-artifact@v4
with:
name: cypress-screenshots
path: frontend/cypress/screenshots
27 changes: 27 additions & 0 deletions backend/coverage_report.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
CODE COVERAGE BEFORE ADDING GEMINI TESTING

Name Stmts Miss Cover
----------------------------------------
main.py 19 4 79%
routers\gemini.py 41 23 44%
routers\recipes.py 26 9 65%
routers\user.py 40 5 88%
test_recipes.py 16 0 100%
test_user.py 65 1 98%
----------------------------------------
TOTAL 207 42 80%

---------------------------------------------
AFTER ADDING GEMINI TESTING

Name Stmts Miss Cover
------------------------------------------
main.py 19 4 79%
routers\gemini.py 41 7 83%
routers\recipes.py 26 9 65%
routers\user.py 40 5 88%
test_gemini_extra.py 56 0 100%
test_recipes.py 16 0 100%
test_user.py 65 1 98%
------------------------------------------
TOTAL 263 26 90%
3 changes: 2 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ httpx
google-generativeai
Pillow
supabase
pytest
pytest
coverage
19 changes: 14 additions & 5 deletions backend/routers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ async def get_user_favorite(request: Request) -> Any:
.single() \
.execute()

if not resp or not resp.data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Supabase query failed"
)

# 3) pull the JSON array of recipes
recipes: List[Any] = resp.data.get("favorite") or []

Expand All @@ -59,8 +65,9 @@ async def set_user_favorite(request: Request, recipe: Dict[str, Any] = Body(...)
.eq("UID", user_id) \
.single() \
.execute()

raw = resp.data.get("favorite")

raw = resp.data.get("favorite") if resp and resp.data else None

# 2) coerce to a list
if raw is None:
current: List[Any] = []
Expand All @@ -70,9 +77,11 @@ async def set_user_favorite(request: Request, recipe: Dict[str, Any] = Body(...)
# if it's a dict (or anything else), wrap it into a one-element list
current = [raw]

# 3) append the new recipe object
updated = current + [recipe]

# 3) check for duplicates and append the new recipe object
if recipe not in current:
updated = current + [recipe]
else:
updated = current

upd = sb.from_("user") \
.update({"favorite": updated}) \
Expand Down
65 changes: 65 additions & 0 deletions backend/test_gemini.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import pytest
from fastapi import HTTPException
from routers import gemini
import types
import asyncio

class DummyResponse:
def __init__(self, text, status_code=200):
self.text = text
self.status_code = status_code
def json(self):
return [{"id": 1, "title": "Dummy Recipe"}]


def test_get_ingredients_from_image_success(monkeypatch):
class DummyModel:
def generate_content(self, args):
class Resp:
text = "tomato, cheese"
return Resp()
monkeypatch.setattr(gemini, "model", DummyModel())
# Moc PIL.Image.open to avoid real image
monkeypatch.setattr(gemini.Image, "open", lambda x: object())
# PNG header + dummy data
image_bytes = b"\x89PNG\r\n\x1a\n" + b"0" * 100
result = asyncio.run(gemini.get_ingredients_from_image(image_bytes))
assert result == ["tomato", "cheese"]

def test_get_ingredients_from_image_error(monkeypatch):
class DummyModel:
def generate_content(self, args):
raise Exception("Gemini error")
monkeypatch.setattr(gemini, "model", DummyModel())
monkeypatch.setattr(gemini.Image, "open", lambda x: object())
image_bytes = b"notanimage"
with pytest.raises(HTTPException) as exc:
asyncio.run(gemini.get_ingredients_from_image(image_bytes))
assert "Error analyzing image" in str(exc.value.detail)

def test_get_recipes_from_ingredients_success(monkeypatch):
def dummy_get(url, params):
return DummyResponse("", 200)
monkeypatch.setattr(gemini.requests, "get", dummy_get)
result = asyncio.run(gemini.get_recipes_from_ingredients(["tomato", "cheese"]))
assert result[0]["title"] == "Dummy Recipe"

def test_get_recipes_from_ingredients_api_error(monkeypatch):
def dummy_get(url, params):
return DummyResponse("", 500)
monkeypatch.setattr(gemini.requests, "get", dummy_get)
with pytest.raises(HTTPException) as exc:
asyncio.run(gemini.get_recipes_from_ingredients(["tomato"]))
assert "Error fetching recipes" in str(exc.value.detail)

def test_get_ingredients_from_image_empty(monkeypatch):
class DummyModel:
def generate_content(self, args):
class Resp:
text = " "
return Resp()
monkeypatch.setattr(gemini, "model", DummyModel())
monkeypatch.setattr(gemini.Image, "open", lambda x: object())
image_bytes = b"\x89PNG\r\n\x1a\n" + b"0" * 100
result = asyncio.run(gemini.get_ingredients_from_image(image_bytes))
assert result == [""]
51 changes: 51 additions & 0 deletions backend/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,54 @@ def test_post_appends_to_list(stub_supabase):
favs = r.json()["favorites"]
assert isinstance(favs, list)
assert existing in favs and new_recipe in favs

# POST /user/favorite and GET /user/favorite Integration Test

def test_post_and_get_favorite(stub_supabase):
stub_supabase.next_resp = FakeResp({"favorite": []})
new_recipe = {"id": 99, "title": "IntegrationTestRecipe"}

r_post = client.post(
"/user/favorite",
headers={"ID": TEST_USER_ID},
json=new_recipe
)
assert r_post.status_code == 200
favs = r_post.json()["favorites"]
assert new_recipe in favs

stub_supabase.next_resp = FakeResp({"favorite": [new_recipe]})
r_get = client.get("/user/favorite", headers={"ID": TEST_USER_ID})
assert r_get.status_code == 200
body = r_get.json()
assert body["user_id"] == TEST_USER_ID
assert new_recipe in body["recipes"]

# Test for error condition: Simulating a Supabase query failure
def test_supabase_query_failure(stub_supabase):
class ErrorResp:
def __init__(self):
self.status_code = 500
self.data = None
def execute(self):
raise Exception("Supabase query failed")

stub_supabase.next_resp = ErrorResp()

r = client.get("/user/favorite", headers={"ID": TEST_USER_ID})
assert r.status_code == 400
assert "Supabase query failed" in r.json()["detail"]

# Test for expected interaction: Preventing duplicate recipes
def test_prevent_duplicate_recipes(stub_supabase):
existing_recipe = {"id": 1, "title": "DuplicateTestRecipe"}
stub_supabase.next_resp = FakeResp({"favorite": [existing_recipe]})

r = client.post(
"/user/favorite",
headers={"ID": TEST_USER_ID},
json=existing_recipe
)
assert r.status_code == 200
favs = r.json()["favorites"]
assert favs == [existing_recipe]
1 change: 1 addition & 0 deletions frontend/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:4173',
screenshotOnRunFailure: true,
},
});
Loading