From 4794e5527496328070bb8e0c8264d192318b209c Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:38:52 -0400 Subject: [PATCH 01/17] update tests --- backend/src/test_api.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/src/test_api.py b/backend/src/test_api.py index 132d44e..556e500 100644 --- a/backend/src/test_api.py +++ b/backend/src/test_api.py @@ -95,22 +95,22 @@ def test_expenses(user_id, category_id): print(f"Error creating expense: {response.text}") return None -def main(): - print("Starting API tests...") - print("=" * 50) - try: - test_health() - user_id = test_user() - category_id = test_categories() - if user_id and category_id: - test_expenses(user_id, category_id) - print("=" * 50) - print("Tests completed!") - except requests.exceptions.ConnectionError: - print("Error: Could not connect to the API. Make sure the services are running with:") - print("docker compose up -d") - except Exception as e: - print(f"Error during testing: {e}") +# def main(): +# print("Starting API tests...") +# print("=" * 50) +# try: +# test_health() +# user_id = test_user() +# category_id = test_categories() +# if user_id and category_id: +# test_expenses(user_id, category_id) +# print("=" * 50) +# print("Tests completed!") +# except requests.exceptions.ConnectionError: +# print("Error: Could not connect to the API. Make sure the services are running with:") +# print("docker compose up -d") +# except Exception as e: +# print(f"Error during testing: {e}") -if __name__ == "__main__": - main() \ No newline at end of file +# if __name__ == "__main__": +# main() \ No newline at end of file From 1e9c0fb8e1aca9a48d876c66858c36e268c754b7 Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:42:35 -0400 Subject: [PATCH 02/17] use ui for coverage --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 7c3b201..9efda07 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,7 @@ "preview": "vite preview", "test": "vitest", "test:ui": "vitest --ui", - "test:coverage": "vitest --coverage", + "test:coverage": "vitest --ui --coverage", "test:watch": "vitest --watch" }, "dependencies": { From bab0748c19888fdaee078ab0b70f919953b8c9bb Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:45:13 -0400 Subject: [PATCH 03/17] ignore project folder --- .gitignore | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 3a5c722..d25dced 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,21 @@ # Mac .DS_Store -# Application +# Project management +.project/ +.cursor/ real_data/ -my_notes.txt -notes.txt + +# Application *log.* -.vite/ -notes/ # Frontend frontend/coverage/ +.vite/ # CSV files - ignore purchases.csv but keep sample data *-purchases.csv -# Cursor -.cursor/ - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] From bd7525f4e10469e9811fc95b9fa0b40f53cf875e Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:35:40 -0400 Subject: [PATCH 04/17] add test:ui:coverage --- frontend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 9efda07..2c89c9e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,8 @@ "preview": "vite preview", "test": "vitest", "test:ui": "vitest --ui", - "test:coverage": "vitest --ui --coverage", + "test:ui:coverage": "vitest --ui --coverage", + "test:coverage": "vitest --coverage", "test:watch": "vitest --watch" }, "dependencies": { From 5cda494be925e4a5aaff4f734a021c2f28fe0475 Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:39:36 -0400 Subject: [PATCH 05/17] move tests to test folder --- backend/test/__init__.py | 0 backend/{src => test}/test_api.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/test/__init__.py rename backend/{src => test}/test_api.py (100%) diff --git a/backend/test/__init__.py b/backend/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/test_api.py b/backend/test/test_api.py similarity index 100% rename from backend/src/test_api.py rename to backend/test/test_api.py From 46dd40b5b58bbe259578dbc0a81bbbb0b664e1d4 Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Tue, 7 Oct 2025 20:20:15 -0400 Subject: [PATCH 06/17] switch to pytest --- backend/test/test_api.py | 110 ++++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 47 deletions(-) diff --git a/backend/test/test_api.py b/backend/test/test_api.py index 556e500..e97a0a5 100644 --- a/backend/test/test_api.py +++ b/backend/test/test_api.py @@ -14,9 +14,8 @@ def test_health(): print("Testing health endpoint...") response = requests.get(f"{BASE_URL}/health") - print(f"Status: {response.status_code}") - print(f"Response: {response.json()}") - print() + assert response.status_code == 200 + assert response.json()["status"] == "healthy" def test_user(): print("Testing user endpoints...") @@ -28,13 +27,11 @@ def test_user(): } response = requests.post(f"{BASE_URL}/users/", json=user_data) print(f"Create user status: {response.status_code}") - if response.status_code == 201: - user = response.json() - print(f"Created user: {user}") - return user["user_id"] - else: - print(f"Error creating user: {response.text}") - return None + assert response.status_code == 201 + user = response.json() + assert user["username"] == "testuser" + assert user["email"] == "testuser@example.com" + assert user["role"] == "regular" def test_categories(): print("Testing category endpoints...") @@ -43,24 +40,30 @@ def test_categories(): } response = requests.post(f"{BASE_URL}/categories/", json=category_data) print(f"Create category status: {response.status_code}") - if response.status_code == 201: - category = response.json() - print(f"Created category: {category}") - category_id = category['category_id'] - # Get all categories - response = requests.get(f"{BASE_URL}/categories/") - print(f"Get categories status: {response.status_code}") - print(f"Categories: {response.json()}") - # Update category - update_data = {"category_name": "Updated Food & Dining"} - response = requests.put(f"{BASE_URL}/categories/{category_id}", json=update_data) - print(f"Update category status: {response.status_code}") - return category_id - else: - print(f"Error creating category: {response.text}") - return None + assert response.status_code == 201 + category = response.json() + assert category["category_name"] == "Food & Dining" + category_id = category['category_id'] + assert category_id is not None + # Get all categories + response = requests.get(f"{BASE_URL}/categories/") + print(f"Get categories status: {response.status_code}") + assert response.status_code == 200 + categories = response.json() + category_names = [category["category_name"] for category in categories] + assert "Food & Dining" in category_names + # Update category + update_data = {"category_name": "Updated Food & Dining"} + response = requests.put(f"{BASE_URL}/categories/{category_id}", json=update_data) + print(f"Update category status: {response.status_code}") + assert response.status_code == 200 + assert response.json()["category_name"] == "Updated Food & Dining" -def test_expenses(user_id, category_id): +# TODO: Test deleting a category + +def test_expenses(): + user_id = test_user() + category_id = test_categories() print("\nTesting expense endpoints...") # Create an expense expense_data = { @@ -73,27 +76,40 @@ def test_expenses(user_id, category_id): "notes": "Delicious burrito bowl" } response = requests.post(f"{BASE_URL}/expenses/", json=expense_data) + assert response.status_code == 201 + expense = response.json() + assert expense["item"] == "Lunch at Chipotle" + assert expense["vendor"] == "Chipotle" + assert expense["price"] == 25.50 + assert expense["date_purchased"] == date.today().isoformat() + assert expense["user_id"] == user_id print(f"Create expense status: {response.status_code}") - if response.status_code == 201: - expense = response.json() - print(f"Created expense: {expense}") - expense_id = expense['expense_id'] - # Link expense to category - response = requests.post(f"{BASE_URL}/expenses/{expense_id}/categories/{category_id}") - print(f"Link expense to category status: {response.status_code}") - # Get all expenses - response = requests.get(f"{BASE_URL}/expenses/") - print(f"Get expenses status: {response.status_code}") - print(f"Expenses: {response.json()}") - # Get analytics - response = requests.get(f"{BASE_URL}/analytics/total") - print(f"Total expenses: {response.json()}") - response = requests.get(f"{BASE_URL}/analytics/by-category") - print(f"Expenses by category: {response.json()}") - return expense_id - else: - print(f"Error creating expense: {response.text}") - return None + expense_id = expense['expense_id'] + assert expense_id is not None + # Link expense to category + response = requests.post(f"{BASE_URL}/expenses/{expense_id}/categories/{category_id}") + assert response.status_code == 201 + assert response.json()["category_id"] == category_id + print(f"Link expense to category status: {response.status_code}") + # Get all expenses + response = requests.get(f"{BASE_URL}/expenses/") + print(f"Get expenses status: {response.status_code}") + assert response.status_code == 200 + assert response.json()[0]["item"] == "Lunch at Chipotle" + assert response.json()[0]["vendor"] == "Chipotle" + assert response.json()[0]["price"] == 25.50 + assert response.json()[0]["date_purchased"] == date.today().isoformat() + assert response.json()[0]["user_id"] == user_id + assert response.json()[0]["category_id"] == category_id + # Get analytics + response = requests.get(f"{BASE_URL}/analytics/total") + assert response.status_code == 200 + assert response.json()["total"] == 25.50 + response = requests.get(f"{BASE_URL}/analytics/by-category") + assert response.status_code == 200 + assert response.json()[0]["category_name"] == "Food & Dining" + assert response.json()[0]["total"] == 25.50 + return expense_id # def main(): # print("Starting API tests...") From f20fcad0ee394c650d4e57706c59e7355dcd52f9 Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Tue, 7 Oct 2025 20:31:42 -0400 Subject: [PATCH 07/17] 1 request per test --- backend/test/test_api.py | 49 +++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/backend/test/test_api.py b/backend/test/test_api.py index e97a0a5..38189af 100644 --- a/backend/test/test_api.py +++ b/backend/test/test_api.py @@ -11,6 +11,12 @@ BASE_URL = "http://localhost:8001" +common_data = { + "category_id": None, + "user_id": None, + "expense_id": None, +} + def test_health(): print("Testing health endpoint...") response = requests.get(f"{BASE_URL}/health") @@ -33,7 +39,7 @@ def test_user(): assert user["email"] == "testuser@example.com" assert user["role"] == "regular" -def test_categories(): +def test_post_categories(): print("Testing category endpoints...") category_data = { "category_name": "Food & Dining" @@ -45,6 +51,9 @@ def test_categories(): assert category["category_name"] == "Food & Dining" category_id = category['category_id'] assert category_id is not None + common_data['category_id'] = category_id + +def test_get_categories(): # Get all categories response = requests.get(f"{BASE_URL}/categories/") print(f"Get categories status: {response.status_code}") @@ -52,8 +61,11 @@ def test_categories(): categories = response.json() category_names = [category["category_name"] for category in categories] assert "Food & Dining" in category_names + +def test_put_categories(): # Update category update_data = {"category_name": "Updated Food & Dining"} + category_id = common_data['category_id'] response = requests.put(f"{BASE_URL}/categories/{category_id}", json=update_data) print(f"Update category status: {response.status_code}") assert response.status_code == 200 @@ -86,30 +98,45 @@ def test_expenses(): print(f"Create expense status: {response.status_code}") expense_id = expense['expense_id'] assert expense_id is not None + common_data['expense_id'] = expense_id + +def test_link_expense_to_category(): # Link expense to category + expense_id = common_data['expense_id'] + category_id = common_data['category_id'] response = requests.post(f"{BASE_URL}/expenses/{expense_id}/categories/{category_id}") assert response.status_code == 201 assert response.json()["category_id"] == category_id print(f"Link expense to category status: {response.status_code}") + +def test_get_expenses(): # Get all expenses response = requests.get(f"{BASE_URL}/expenses/") print(f"Get expenses status: {response.status_code}") assert response.status_code == 200 - assert response.json()[0]["item"] == "Lunch at Chipotle" - assert response.json()[0]["vendor"] == "Chipotle" - assert response.json()[0]["price"] == 25.50 - assert response.json()[0]["date_purchased"] == date.today().isoformat() - assert response.json()[0]["user_id"] == user_id - assert response.json()[0]["category_id"] == category_id + assert type(response.json()) == list + assert len(response.json()) > 0 + +def test_get_analytics(): # Get analytics response = requests.get(f"{BASE_URL}/analytics/total") assert response.status_code == 200 - assert response.json()["total"] == 25.50 + total = response.json() + assert "total" in total + assert type(total["total"]) == float + +def test_get_analytics_by_category(): response = requests.get(f"{BASE_URL}/analytics/by-category") assert response.status_code == 200 - assert response.json()[0]["category_name"] == "Food & Dining" - assert response.json()[0]["total"] == 25.50 - return expense_id + by_category = response.json() + assert type(by_category) == list + assert len(by_category) > 0 + some_category = by_category[0] + assert "category" in some_category + assert "total" in some_category + assert type(some_category["total"]) == float + assert type(some_category["category"]) == str + # def main(): # print("Starting API tests...") From 09bdb09091ab1a1541635b8db45cdb77c09bfea0 Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:35:16 -0400 Subject: [PATCH 08/17] =?UTF-8?q?=F0=9F=A4=96:=20use=20test=20db?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/test_api.py | 261 +++++++++++++++++++++++---------------- compose.test.yaml | 17 +++ 2 files changed, 170 insertions(+), 108 deletions(-) create mode 100644 compose.test.yaml diff --git a/backend/test/test_api.py b/backend/test/test_api.py index 38189af..086b358 100644 --- a/backend/test/test_api.py +++ b/backend/test/test_api.py @@ -1,15 +1,75 @@ #!/usr/bin/env python3 """ -Simple test script to verify the FastAPI endpoints -Run this after starting the services with: docker compose up -d +Pytest API tests using FastAPI TestClient bound to a test database on port 5433. +Requires the test Postgres in compose.test.yaml to be running (docker compose -f compose.test.yaml up -d). """ -import requests -import json -from datetime import datetime, date -import uuid +import os +from datetime import date + +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine, event +from sqlalchemy.orm import sessionmaker + +# Import app and DB utilities from the backend src +from main import app # ensure PYTHONPATH includes backend/src when running pytest +from database import Base, get_db + + +# Configure test database (port 5433) +POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD", "password") +TEST_DATABASE_URL = os.getenv( + "TEST_DATABASE_URL", + f"postgresql://postgres:{POSTGRES_PASSWORD}@localhost:5433/test_expenses_db", +) + +engine = create_engine(TEST_DATABASE_URL, pool_pre_ping=True, future=True) +TestingSessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False) + + +@pytest.fixture(scope="session", autouse=True) +def _create_schema(): + Base.metadata.create_all(bind=engine) + yield + Base.metadata.drop_all(bind=engine) + + +@pytest.fixture() +def db_session(): + connection = engine.connect() + trans = connection.begin() + session = TestingSessionLocal(bind=connection) + + nested = connection.begin_nested() + + @event.listens_for(session, "after_transaction_end") + def _restart_savepoint(sess, transaction): + if transaction.nested and not transaction._parent.nested: + sess.connection().begin_nested() + + try: + yield session + finally: + session.close() + nested.rollback() + trans.rollback() + connection.close() + + +@pytest.fixture() +def client(db_session): + def override_get_db(): + try: + yield db_session + finally: + pass + + app.dependency_overrides[get_db] = override_get_db + with TestClient(app) as c: + yield c + app.dependency_overrides.clear() -BASE_URL = "http://localhost:8001" common_data = { "category_id": None, @@ -17,125 +77,110 @@ "expense_id": None, } -def test_health(): - print("Testing health endpoint...") - response = requests.get(f"{BASE_URL}/health") - assert response.status_code == 200 - assert response.json()["status"] == "healthy" -def test_user(): - print("Testing user endpoints...") +def test_health(client): + r = client.get("/health") + assert r.status_code == 200 + assert r.json()["status"] == "healthy" + + +def test_user_flow(client): user_data = { "username": "testuser", "email": "testuser@example.com", "role": "regular", - "password_hash": "fakehash123" + "password_hash": "fakehash123", } - response = requests.post(f"{BASE_URL}/users/", json=user_data) - print(f"Create user status: {response.status_code}") - assert response.status_code == 201 - user = response.json() + r = client.post("/users/", json=user_data) + assert r.status_code == 201 + user = r.json() assert user["username"] == "testuser" assert user["email"] == "testuser@example.com" assert user["role"] == "regular" + common_data["user_id"] = user["user_id"] -def test_post_categories(): - print("Testing category endpoints...") - category_data = { - "category_name": "Food & Dining" - } - response = requests.post(f"{BASE_URL}/categories/", json=category_data) - print(f"Create category status: {response.status_code}") - assert response.status_code == 201 - category = response.json() + +def test_category_crud(client): + # Create + r = client.post("/categories/", json={"category_name": "Food & Dining"}) + assert r.status_code == 201 + category = r.json() assert category["category_name"] == "Food & Dining" - category_id = category['category_id'] - assert category_id is not None - common_data['category_id'] = category_id - -def test_get_categories(): - # Get all categories - response = requests.get(f"{BASE_URL}/categories/") - print(f"Get categories status: {response.status_code}") - assert response.status_code == 200 - categories = response.json() - category_names = [category["category_name"] for category in categories] - assert "Food & Dining" in category_names - -def test_put_categories(): - # Update category - update_data = {"category_name": "Updated Food & Dining"} - category_id = common_data['category_id'] - response = requests.put(f"{BASE_URL}/categories/{category_id}", json=update_data) - print(f"Update category status: {response.status_code}") - assert response.status_code == 200 - assert response.json()["category_name"] == "Updated Food & Dining" - -# TODO: Test deleting a category - -def test_expenses(): - user_id = test_user() - category_id = test_categories() - print("\nTesting expense endpoints...") - # Create an expense - expense_data = { - "item": "Lunch at Chipotle", - "vendor": "Chipotle", - "price": 25.50, - "date_purchased": date.today().isoformat(), - "user_id": user_id, - "payment_method": "credit card", - "notes": "Delicious burrito bowl" - } - response = requests.post(f"{BASE_URL}/expenses/", json=expense_data) - assert response.status_code == 201 - expense = response.json() + common_data["category_id"] = category["category_id"] + + # List + r = client.get("/categories/") + assert r.status_code == 200 + names = [c["category_name"] for c in r.json()] + assert "Food & Dining" in names + + # Update + r = client.put( + f"/categories/{common_data['category_id']}", + json={"category_name": "Updated Food & Dining"}, + ) + assert r.status_code == 200 + assert r.json()["category_name"] == "Updated Food & Dining" + + +def test_expenses_flow(client): + # Ensure user and category exist + test_user_flow(client) + test_category_crud(client) + + # Create expense + r = client.post( + "/expenses/", + json={ + "item": "Lunch at Chipotle", + "vendor": "Chipotle", + "price": 25.50, + "date_purchased": date.today().isoformat(), + "user_id": common_data["user_id"], + "payment_method": "credit card", + "notes": "Delicious burrito bowl", + }, + ) + assert r.status_code == 201 + expense = r.json() assert expense["item"] == "Lunch at Chipotle" assert expense["vendor"] == "Chipotle" assert expense["price"] == 25.50 assert expense["date_purchased"] == date.today().isoformat() - assert expense["user_id"] == user_id - print(f"Create expense status: {response.status_code}") - expense_id = expense['expense_id'] - assert expense_id is not None - common_data['expense_id'] = expense_id + assert expense["user_id"] == common_data["user_id"] + common_data["expense_id"] = expense["expense_id"] -def test_link_expense_to_category(): # Link expense to category - expense_id = common_data['expense_id'] - category_id = common_data['category_id'] - response = requests.post(f"{BASE_URL}/expenses/{expense_id}/categories/{category_id}") - assert response.status_code == 201 - assert response.json()["category_id"] == category_id - print(f"Link expense to category status: {response.status_code}") - -def test_get_expenses(): - # Get all expenses - response = requests.get(f"{BASE_URL}/expenses/") - print(f"Get expenses status: {response.status_code}") - assert response.status_code == 200 - assert type(response.json()) == list - assert len(response.json()) > 0 - -def test_get_analytics(): - # Get analytics - response = requests.get(f"{BASE_URL}/analytics/total") - assert response.status_code == 200 - total = response.json() + r = client.post( + f"/expenses/{common_data['expense_id']}/categories/{common_data['category_id']}" + ) + assert r.status_code == 201 + assert r.json()["category_id"] == common_data["category_id"] + + # List expenses + r = client.get("/expenses/") + assert r.status_code == 200 + assert isinstance(r.json(), list) + assert len(r.json()) > 0 + + +def test_analytics(client): + r = client.get("/analytics/total") + assert r.status_code == 200 + total = r.json() assert "total" in total - assert type(total["total"]) == float - -def test_get_analytics_by_category(): - response = requests.get(f"{BASE_URL}/analytics/by-category") - assert response.status_code == 200 - by_category = response.json() - assert type(by_category) == list - assert len(by_category) > 0 - some_category = by_category[0] - assert "category" in some_category - assert "total" in some_category - assert type(some_category["total"]) == float - assert type(some_category["category"]) == str + assert isinstance(total["total"], float) + + r = client.get("/analytics/by-category") + assert r.status_code == 200 + data = r.json() + assert isinstance(data, list) + if data: + first = data[0] + assert "category" in first + assert "total" in first + assert isinstance(first["total"], float) + assert isinstance(first["category"], str) # def main(): diff --git a/compose.test.yaml b/compose.test.yaml new file mode 100644 index 0000000..2825467 --- /dev/null +++ b/compose.test.yaml @@ -0,0 +1,17 @@ +name: "test-expense-tracking" +services: + db: + image: postgres:17-alpine + restart: always + environment: + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: test_expenses_db + ports: + - "5433:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -d test_expenses_db -U postgres"] + interval: 1s + timeout: 5s + retries: 10 + volumes: + - ./scripts/sql/:/docker-entrypoint-initdb.d/ From 3971837b522f19e9a8172118837dfd791633b408 Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:35:28 -0400 Subject: [PATCH 09/17] remove init.py --- backend/test/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 backend/test/__init__.py diff --git a/backend/test/__init__.py b/backend/test/__init__.py deleted file mode 100644 index e69de29..0000000 From ffccf448ab66dc0ccfe595a90339794630e8ec01 Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:37:08 -0400 Subject: [PATCH 10/17] remove old test code --- backend/test/test_api.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/backend/test/test_api.py b/backend/test/test_api.py index 086b358..71df422 100644 --- a/backend/test/test_api.py +++ b/backend/test/test_api.py @@ -181,24 +181,3 @@ def test_analytics(client): assert "total" in first assert isinstance(first["total"], float) assert isinstance(first["category"], str) - - -# def main(): -# print("Starting API tests...") -# print("=" * 50) -# try: -# test_health() -# user_id = test_user() -# category_id = test_categories() -# if user_id and category_id: -# test_expenses(user_id, category_id) -# print("=" * 50) -# print("Tests completed!") -# except requests.exceptions.ConnectionError: -# print("Error: Could not connect to the API. Make sure the services are running with:") -# print("docker compose up -d") -# except Exception as e: -# print(f"Error during testing: {e}") - -# if __name__ == "__main__": -# main() \ No newline at end of file From ce904d4fb55b30431b89e4cb95f0ad8b82f21783 Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:39:10 -0400 Subject: [PATCH 11/17] testing requirements --- backend/test/requirements.txt | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 backend/test/requirements.txt diff --git a/backend/test/requirements.txt b/backend/test/requirements.txt new file mode 100644 index 0000000..bd5f874 --- /dev/null +++ b/backend/test/requirements.txt @@ -0,0 +1,29 @@ +annotated-types==0.7.0 +anyio==4.11.0 +certifi==2025.10.5 +charset-normalizer==3.4.3 +click==8.3.0 +dnspython==2.8.0 +email-validator==2.3.0 +fastapi==0.118.0 +greenlet==3.2.4 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +idna==3.10 +iniconfig==2.1.0 +packaging==25.0 +pluggy==1.6.0 +psycopg2-binary==2.9.10 +pydantic==2.12.0 +pydantic_core==2.41.1 +Pygments==2.19.2 +pytest==8.4.2 +requests==2.32.5 +sniffio==1.3.1 +SQLAlchemy==2.0.43 +starlette==0.48.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.5.0 +uvicorn==0.37.0 From 0ac59775e47006e4965f31731dda838cfe9197aa Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Wed, 8 Oct 2025 00:21:56 -0400 Subject: [PATCH 12/17] use pytest ini --- backend/pytest.ini | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 backend/pytest.ini diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..fcccae1 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = src From 7c84aa3b8f684ab6f9f8036eff56a201e816765b Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:16:58 -0400 Subject: [PATCH 13/17] fix last errors --- backend/src/crud.py | 18 ++++++++++++------ backend/test/test_api.py | 8 +++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/backend/src/crud.py b/backend/src/crud.py index 9aea6ec..175302a 100644 --- a/backend/src/crud.py +++ b/backend/src/crud.py @@ -469,12 +469,18 @@ def get_total_expenses_in_date_range(db: Session, user_id: Optional[UUID] = None return result or 0.0 def get_expenses_by_category(db: Session, user_id: Optional[UUID] = None) -> List[dict]: - query = db.query( - Category.category_name, - func.sum(Expense.price).label('total') - ).join(ExpenseCategory).join(Expense) - + """Aggregate expenses by category with explicit join path to avoid ambiguity.""" + query = ( + db.query( + Category.category_name, + func.sum(Expense.price).label('total') + ) + .select_from(Category) + .join(ExpenseCategory, ExpenseCategory.category_id == Category.category_id) + .join(Expense, Expense.expense_id == ExpenseCategory.expense_id) + ) + if user_id: query = query.filter(Expense.user_id == user_id) - + return query.group_by(Category.category_id, Category.category_name).all() \ No newline at end of file diff --git a/backend/test/test_api.py b/backend/test/test_api.py index 71df422..708a265 100644 --- a/backend/test/test_api.py +++ b/backend/test/test_api.py @@ -66,7 +66,9 @@ def override_get_db(): pass app.dependency_overrides[get_db] = override_get_db - with TestClient(app) as c: + # Use localhost as base_url so TrustedHostMiddleware allows requests + # with TestClient(app) as c: + with TestClient(app, base_url="http://localhost") as c: yield c app.dependency_overrides.clear() @@ -89,7 +91,7 @@ def test_user_flow(client): "username": "testuser", "email": "testuser@example.com", "role": "regular", - "password_hash": "fakehash123", + "password_hash": "x" * 60 } r = client.post("/users/", json=user_data) assert r.status_code == 201 @@ -154,7 +156,7 @@ def test_expenses_flow(client): r = client.post( f"/expenses/{common_data['expense_id']}/categories/{common_data['category_id']}" ) - assert r.status_code == 201 + assert r.status_code == 200 assert r.json()["category_id"] == common_data["category_id"] # List expenses From f1b19cbb4aeaeb5ad120d272236a91b8ea4aeb35 Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:23:07 -0400 Subject: [PATCH 14/17] bump pydantic --- backend/src/schemas.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/backend/src/schemas.py b/backend/src/schemas.py index e3ebffb..d344c83 100644 --- a/backend/src/schemas.py +++ b/backend/src/schemas.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, Field, validator, EmailStr +from pydantic import field_validator, ConfigDict, BaseModel, Field, validator, EmailStr from typing import Optional, List from datetime import datetime, date from uuid import UUID @@ -23,15 +23,14 @@ class User(UserBase): user_id: UUID created_at: datetime last_login: datetime - - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) # Category schemas class CategoryBase(BaseModel): category_name: str = Field(..., min_length=1, max_length=100, strip_whitespace=True) - @validator('category_name') + @field_validator('category_name') + @classmethod def validate_category_name(cls, v): # Remove any potentially dangerous characters v = re.sub(r'[<>"\']', '', v) @@ -45,9 +44,7 @@ class CategoryUpdate(CategoryBase): class Category(CategoryBase): category_id: UUID - - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) # Expense schemas class ExpenseBase(BaseModel): @@ -58,7 +55,8 @@ class ExpenseBase(BaseModel): payment_method: Optional[str] = Field(None, max_length=100, strip_whitespace=True) notes: Optional[str] = Field(None, max_length=1000, strip_whitespace=True) - @validator('item', 'vendor', 'payment_method', 'notes') + @field_validator('item', 'vendor', 'payment_method', 'notes') + @classmethod def validate_text_fields(cls, v): if v is not None: # Remove potentially dangerous characters @@ -84,9 +82,7 @@ class Expense(ExpenseBase): user_id: UUID created_at: datetime categories: Optional[List[Category]] = [] - - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) # ExpenseCategory schemas class ExpenseCategoryBase(BaseModel): @@ -97,8 +93,7 @@ class ExpenseCategoryCreate(ExpenseCategoryBase): pass class ExpenseCategory(ExpenseCategoryBase): - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) # Wishlist schemas class WishlistBase(BaseModel): @@ -110,7 +105,8 @@ class WishlistBase(BaseModel): notes: Optional[str] = Field(None, max_length=1000, strip_whitespace=True) planned_date: Optional[date] = None - @validator('item', 'vendor', 'notes') + @field_validator('item', 'vendor', 'notes') + @classmethod def validate_text_fields(cls, v): if v is not None: v = re.sub(r'[<>"\']', '', v) @@ -134,9 +130,7 @@ class Wishlist(WishlistBase): user_id: UUID created_at: Optional[datetime] = None user: Optional[User] = None - - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) # Budget schemas class BudgetBase(BaseModel): @@ -148,12 +142,16 @@ class BudgetBase(BaseModel): timeframe_interval: Optional[int] = Field(None, ge=1, le=100) recurring_start_date: Optional[date] = None + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. @validator('end_date') def validate_date_range(cls, v, values): if 'start_date' in values and v <= values['start_date']: raise ValueError('end_date must be after start_date') return v + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. @validator('timeframe_interval') def validate_timeframe_interval(cls, v, values): if 'timeframe_type' in values and values['timeframe_type'] != 'custom' and v is None: @@ -181,6 +179,4 @@ class Budget(BudgetBase): category_id: UUID current_spend: float = Field(..., ge=0, le=999999.99) future_spend: float = Field(..., ge=0, le=999999.99) - - class Config: - from_attributes = True \ No newline at end of file + model_config = ConfigDict(from_attributes=True) \ No newline at end of file From 80ee0301db071a2c899150e4e7cab5e4620255bc Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:35:36 -0400 Subject: [PATCH 15/17] resolve deprecations --- backend/src/crud.py | 20 ++++++++++---------- backend/src/database.py | 3 +-- backend/src/schemas.py | 40 ++++++++++++++++++++-------------------- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/backend/src/crud.py b/backend/src/crud.py index 175302a..91cc24e 100644 --- a/backend/src/crud.py +++ b/backend/src/crud.py @@ -77,7 +77,7 @@ def get_users(db: Session, skip: int = 0, limit: int = 100) -> List[User]: def create_user(db: Session, user: UserCreate) -> User: # Sanitize input - user_data = user.dict() + user_data = user.model_dump() user_data['username'] = sanitize_input(user_data['username']) user_data['email'] = sanitize_input(user_data['email']) @@ -90,7 +90,7 @@ def create_user(db: Session, user: UserCreate) -> User: def update_user(db: Session, user_id: UUID, user: UserUpdate) -> Optional[User]: db_user = get_user(db, user_id) if db_user: - update_data = user.dict(exclude_unset=True) + update_data = user.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(db_user, field, value) db.commit() @@ -134,7 +134,7 @@ def get_or_create_category(db: Session, category_name: str) -> Category: def create_category(db: Session, category: CategoryCreate) -> Category: # Sanitize input - category_data = category.dict() + category_data = category.model_dump() category_data['category_name'] = sanitize_input(category_data['category_name']) db_category = Category(**category_data) @@ -146,7 +146,7 @@ def create_category(db: Session, category: CategoryCreate) -> Category: def update_category(db: Session, category_id: UUID, category: CategoryUpdate) -> Optional[Category]: db_category = get_category(db, category_id) if db_category: - update_data = category.dict(exclude_unset=True) + update_data = category.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(db_category, field, value) db.commit() @@ -181,7 +181,7 @@ def get_expenses( def create_expense(db: Session, expense: ExpenseCreate) -> Expense: # Sanitize input - expense_data = expense.dict() + expense_data = expense.model_dump() expense_data['item'] = sanitize_input(expense_data['item']) expense_data['vendor'] = sanitize_input(expense_data['vendor']) if expense_data.get('payment_method'): @@ -214,7 +214,7 @@ def create_expense(db: Session, expense: ExpenseCreate) -> Expense: def update_expense(db: Session, expense_id: UUID, expense: ExpenseUpdate) -> Optional[Expense]: db_expense = get_expense(db, expense_id) if db_expense: - update_data = expense.dict(exclude_unset=True) + update_data = expense.model_dump(exclude_unset=True) # Extract new categories before updating expense new_categories = update_data.pop('new_categories', []) or [] @@ -335,7 +335,7 @@ def get_wishlist_items( return query.order_by(Wishlist.priority).offset(skip).limit(limit).all() def create_wishlist_item(db: Session, wishlist_item: WishlistCreate) -> Wishlist: - db_wishlist_item = Wishlist(**wishlist_item.dict()) + db_wishlist_item = Wishlist(**wishlist_item.model_dump()) db.add(db_wishlist_item) db.commit() db.refresh(db_wishlist_item) @@ -344,7 +344,7 @@ def create_wishlist_item(db: Session, wishlist_item: WishlistCreate) -> Wishlist def update_wishlist_item(db: Session, wish_id: UUID, wishlist_item: WishlistUpdate) -> Optional[Wishlist]: db_wishlist_item = get_wishlist_item(db, wish_id) if db_wishlist_item: - update_data = wishlist_item.dict(exclude_unset=True) + update_data = wishlist_item.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(db_wishlist_item, field, value) db.commit() @@ -397,7 +397,7 @@ def create_budget(db: Session, budget: BudgetCreate) -> Budget: is_over_max = total_spend > budget.max_spend # Create budget with calculated values - budget_data = budget.dict() + budget_data = budget.model_dump() budget_data['current_spend'] = current_spend budget_data['future_spend'] = future_spend budget_data['is_over_max'] = is_over_max @@ -413,7 +413,7 @@ def create_budget(db: Session, budget: BudgetCreate) -> Budget: def update_budget(db: Session, budget_id: UUID, budget: BudgetUpdate) -> Optional[Budget]: db_budget = get_budget(db, budget_id) if db_budget: - update_data = budget.dict(exclude_unset=True) + update_data = budget.model_dump(exclude_unset=True) # Recalculate current spend if category or user changed if 'category_id' in update_data or 'user_id' in update_data: diff --git a/backend/src/database.py b/backend/src/database.py index cfc278e..fa2e870 100644 --- a/backend/src/database.py +++ b/backend/src/database.py @@ -1,6 +1,5 @@ from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import sessionmaker, declarative_base import os # Database URL from environment variable diff --git a/backend/src/schemas.py b/backend/src/schemas.py index d344c83..343355d 100644 --- a/backend/src/schemas.py +++ b/backend/src/schemas.py @@ -8,7 +8,7 @@ class UserBase(BaseModel): username: str = Field(..., min_length=3, max_length=50, pattern=r'^[a-zA-Z0-9_]+$') email: EmailStr - role: str = Field(..., pattern=r'^(regular|admin)$', strip_whitespace=True) + role: str = Field(..., pattern=r'^(regular|admin)$', json_schema_extra={'strip_whitespace': True}) class UserCreate(UserBase): password_hash: str = Field(..., min_length=60, max_length=73) # bcrypt hash length @@ -16,7 +16,7 @@ class UserCreate(UserBase): class UserUpdate(UserBase): username: Optional[str] = Field(None, min_length=3, max_length=50, pattern=r'^[a-zA-Z0-9_]+$') email: Optional[EmailStr] = None - role: Optional[str] = Field(None, pattern=r'^(regular|admin)$', strip_whitespace=True) + role: Optional[str] = Field(None, pattern=r'^(regular|admin)$', json_schema_extra={'strip_whitespace': True}) password_hash: Optional[str] = Field(None, min_length=60, max_length=73) class User(UserBase): @@ -27,7 +27,7 @@ class User(UserBase): # Category schemas class CategoryBase(BaseModel): - category_name: str = Field(..., min_length=1, max_length=100, strip_whitespace=True) + category_name: str = Field(..., min_length=1, max_length=100, json_schema_extra={'strip_whitespace': True}) @field_validator('category_name') @classmethod @@ -40,7 +40,7 @@ class CategoryCreate(CategoryBase): pass class CategoryUpdate(CategoryBase): - category_name: Optional[str] = Field(None, min_length=1, max_length=100, strip_whitespace=True) + category_name: Optional[str] = Field(None, min_length=1, max_length=100, json_schema_extra={'strip_whitespace': True}) class Category(CategoryBase): category_id: UUID @@ -48,12 +48,12 @@ class Category(CategoryBase): # Expense schemas class ExpenseBase(BaseModel): - item: str = Field(..., min_length=1, max_length=255, strip_whitespace=True) - vendor: str = Field(..., min_length=1, max_length=255, strip_whitespace=True) + item: str = Field(..., min_length=1, max_length=255, json_schema_extra={'strip_whitespace': True}) + vendor: str = Field(..., min_length=1, max_length=255, json_schema_extra={'strip_whitespace': True}) price: float = Field(..., gt=0, le=999999.99) # Positive price with reasonable limit date_purchased: date - payment_method: Optional[str] = Field(None, max_length=100, strip_whitespace=True) - notes: Optional[str] = Field(None, max_length=1000, strip_whitespace=True) + payment_method: Optional[str] = Field(None, max_length=100, json_schema_extra={'strip_whitespace': True}) + notes: Optional[str] = Field(None, max_length=1000, json_schema_extra={'strip_whitespace': True}) @field_validator('item', 'vendor', 'payment_method', 'notes') @classmethod @@ -69,12 +69,12 @@ class ExpenseCreate(ExpenseBase): new_categories: Optional[List[str]] = Field(None, description="List of new category names to create") class ExpenseUpdate(ExpenseBase): - item: Optional[str] = Field(None, min_length=1, max_length=255, strip_whitespace=True) - vendor: Optional[str] = Field(None, min_length=1, max_length=255, strip_whitespace=True) + item: Optional[str] = Field(None, min_length=1, max_length=255, json_schema_extra={'strip_whitespace': True}) + vendor: Optional[str] = Field(None, min_length=1, max_length=255, json_schema_extra={'strip_whitespace': True}) price: Optional[float] = Field(None, gt=0, le=999999.99) date_purchased: Optional[date] = None - payment_method: Optional[str] = Field(None, max_length=100, strip_whitespace=True) - notes: Optional[str] = Field(None, max_length=1000, strip_whitespace=True) + payment_method: Optional[str] = Field(None, max_length=100, json_schema_extra={'strip_whitespace': True}) + notes: Optional[str] = Field(None, max_length=1000, json_schema_extra={'strip_whitespace': True}) new_categories: Optional[List[str]] = Field(None, description="List of new category names to create") class Expense(ExpenseBase): @@ -97,12 +97,12 @@ class ExpenseCategory(ExpenseCategoryBase): # Wishlist schemas class WishlistBase(BaseModel): - item: str = Field(..., min_length=1, max_length=255, strip_whitespace=True) - vendor: Optional[str] = Field(None, max_length=255, strip_whitespace=True) + item: str = Field(..., min_length=1, max_length=255, json_schema_extra={'strip_whitespace': True}) + vendor: Optional[str] = Field(None, max_length=255, json_schema_extra={'strip_whitespace': True}) price: float = Field(..., gt=0, le=999999.99) priority: int = Field(..., ge=1, le=10) # Priority 1-10 status: str = Field(..., pattern=r'^(wished|scheduled|bought)$') - notes: Optional[str] = Field(None, max_length=1000, strip_whitespace=True) + notes: Optional[str] = Field(None, max_length=1000, json_schema_extra={'strip_whitespace': True}) planned_date: Optional[date] = None @field_validator('item', 'vendor', 'notes') @@ -117,12 +117,12 @@ class WishlistCreate(WishlistBase): user_id: UUID class WishlistUpdate(WishlistBase): - item: Optional[str] = Field(None, min_length=1, max_length=255, strip_whitespace=True) - vendor: Optional[str] = Field(None, max_length=255, strip_whitespace=True) + item: Optional[str] = Field(None, min_length=1, max_length=255, json_schema_extra={'strip_whitespace': True}) + vendor: Optional[str] = Field(None, max_length=255, json_schema_extra={'strip_whitespace': True}) price: Optional[float] = Field(None, gt=0, le=999999.99) priority: Optional[int] = Field(None, ge=1, le=10) status: Optional[str] = Field(None, pattern=r'^(wished|scheduled|bought)$') - notes: Optional[str] = Field(None, max_length=1000, strip_whitespace=True) + notes: Optional[str] = Field(None, max_length=1000, json_schema_extra={'strip_whitespace': True}) planned_date: Optional[date] = None class Wishlist(WishlistBase): @@ -144,7 +144,7 @@ class BudgetBase(BaseModel): # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. - @validator('end_date') + @field_validator('end_date') def validate_date_range(cls, v, values): if 'start_date' in values and v <= values['start_date']: raise ValueError('end_date must be after start_date') @@ -152,7 +152,7 @@ def validate_date_range(cls, v, values): # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. - @validator('timeframe_interval') + @field_validator('timeframe_interval') def validate_timeframe_interval(cls, v, values): if 'timeframe_type' in values and values['timeframe_type'] != 'custom' and v is None: raise ValueError('timeframe_interval is required for non-custom timeframes') From 14cf5e85c60c70f681df6ed4ed4015dadc66e0d5 Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:52:00 -0400 Subject: [PATCH 16/17] =?UTF-8?q?=F0=9F=A4=96:=20resolve=20pydantic=20sche?= =?UTF-8?q?ma=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/models.py b/backend/src/models.py index 7a817a6..fb64714 100644 --- a/backend/src/models.py +++ b/backend/src/models.py @@ -30,7 +30,7 @@ class Category(Base): # Relationships expense_categories = relationship("ExpenseCategory", back_populates="category") budgets = relationship("Budget", back_populates="category") - expenses = relationship("Expense", secondary="expense_categories", back_populates="categories") + expenses = relationship("Expense", secondary="expense_categories", back_populates="categories", overlaps="expense_categories") class Expense(Base): __tablename__ = "expenses" @@ -47,8 +47,8 @@ class Expense(Base): # Relationships user = relationship("User", back_populates="expenses") - expense_categories = relationship("ExpenseCategory", back_populates="expense") - categories = relationship("Category", secondary="expense_categories", back_populates="expenses") + expense_categories = relationship("ExpenseCategory", back_populates="expense", overlaps="expenses") + categories = relationship("Category", secondary="expense_categories", back_populates="expenses", overlaps="expense_categories") class ExpenseCategory(Base): __tablename__ = "expense_categories" From 28f60c6c8f5230b0ac25dab202ded6d5485bb0f7 Mon Sep 17 00:00:00 2001 From: Cherise Bryan <51966982+CheriseCodes@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:22:27 -0400 Subject: [PATCH 17/17] dont clash with other .project folders --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d25dced..429e471 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ .DS_Store # Project management -.project/ +.my_project/ .cursor/ real_data/