diff --git a/.github/workflows/E2E.yml b/.github/workflows/E2E.yml
index be21126..82815e3 100644
--- a/.github/workflows/E2E.yml
+++ b/.github/workflows/E2E.yml
@@ -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
@@ -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
\ No newline at end of file
diff --git a/.github/workflows/arihant.yml b/.github/workflows/arihant.yml
new file mode 100644
index 0000000..b99d2dd
--- /dev/null
+++ b/.github/workflows/arihant.yml
@@ -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
diff --git a/backend/coverage_report.txt b/backend/coverage_report.txt
new file mode 100644
index 0000000..0778fab
--- /dev/null
+++ b/backend/coverage_report.txt
@@ -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%
\ No newline at end of file
diff --git a/backend/requirements.txt b/backend/requirements.txt
index f15b6f5..3a87ffc 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -6,4 +6,5 @@ httpx
google-generativeai
Pillow
supabase
-pytest
\ No newline at end of file
+pytest
+coverage
\ No newline at end of file
diff --git a/backend/routers/user.py b/backend/routers/user.py
index 445a52a..3b999f5 100644
--- a/backend/routers/user.py
+++ b/backend/routers/user.py
@@ -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 []
@@ -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] = []
@@ -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}) \
diff --git a/backend/test_gemini.py b/backend/test_gemini.py
new file mode 100644
index 0000000..4c2040e
--- /dev/null
+++ b/backend/test_gemini.py
@@ -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 == [""]
diff --git a/backend/test_user.py b/backend/test_user.py
index a0f2d35..923138a 100644
--- a/backend/test_user.py
+++ b/backend/test_user.py
@@ -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]
\ No newline at end of file
diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts
index 6feaf18..46ffdf4 100644
--- a/frontend/cypress.config.ts
+++ b/frontend/cypress.config.ts
@@ -3,5 +3,6 @@ const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:4173',
+ screenshotOnRunFailure: true,
},
});
\ No newline at end of file
diff --git a/frontend/cypress/e2e/Auth.cy.ts b/frontend/cypress/e2e/Auth.cy.ts
new file mode 100644
index 0000000..0f0569c
--- /dev/null
+++ b/frontend/cypress/e2e/Auth.cy.ts
@@ -0,0 +1,34 @@
+///