diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f869091 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +name: CI Pipeline + +# Trigger this workflow on Push to specific branches OR on Pull Requests to main +on: + push: + branches: [ "dev" ] + pull_request: + branches: [ "main" ] + +jobs: + # --- JOB 1: Test the Frontend --- + frontend-build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./client + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'npm' + cache-dependency-path: client/package-lock.json + + - name: Install Dependencies + run: npm ci + + - name: Build (Checks for Type Errors) + run: npm run build + env: + # We add dummy vars because the build needs them, + # we don't need real secrets just to check syntax. + NEXT_PUBLIC_API_URL: "http://localhost:8000" + NEXT_PUBLIC_FIREBASE_API_KEY: "dummy" + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: "dummy" + NEXT_PUBLIC_FIREBASE_PROJECT_ID: "dummy" + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: "dummy" + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: "dummy" + NEXT_PUBLIC_FIREBASE_APP_ID: "dummy" + + # --- JOB 2: Test the Backend --- + backend-test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./server + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.13' + cache: 'pip' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install httpx pytest # Ensure test tools are installed + + - name: Run Tests + # We skip tests that require real Gemini credentials for now + # using the '-m "not integration"' marker if we had one, + # or just basic unit tests. + run: | + pytest + env: + # Dummy keys so the app creates the 'client' object without crashing + GEMINI_API_KEY: "dummy_key" + GOOGLE_APPLICATION_CREDENTIALS: "dummy_creds.json" \ No newline at end of file diff --git a/pytest.ini b/server/pytest.ini similarity index 100% rename from pytest.ini rename to server/pytest.ini diff --git a/server/tests/test_main.py b/server/tests/test_main.py index 485fb7c..7445c5c 100644 --- a/server/tests/test_main.py +++ b/server/tests/test_main.py @@ -1,6 +1,6 @@ import pytest from fastapi.testclient import TestClient -from server.main import app +from main import app @pytest.fixture(autouse=True) @@ -10,30 +10,35 @@ def mock_firebase(mocker): The autouse=True means this runs automatically for all tests. """ - # Create our mock Firestore client + # Create the Mock DB Client class MockDocRef: id = "fake_doc_id_123" mock_db_client = mocker.Mock() + # Ensure nested calls like db.collection().add() return valid mocks mock_db_client.collection.return_value.add.return_value = (None, MockDocRef()) + mock_db_client.collection.return_value.document.return_value = mocker.Mock() - # Mock the components in the correct order (before client creation) + # Patch the External Firebase Calls (so they don't hit the network) mocker.patch("firebase_admin.credentials.Certificate") mocker.patch("firebase_admin.initialize_app") mock_fs_client = mocker.patch("firebase_admin.firestore.client") mock_fs_client.return_value = mock_db_client - # IMPORTANT: Set the global db variable in main.py - import server.main + # PATCH THE GLOBAL DB VARIABLE + import main - server.main.db = mock_db_client + # We overwrite the 'db' variable inside the 'main' module + main.db = mock_db_client - # Clean up after the test yield - server.main.db = None + # 4. Clean up + main.db = None -# Now create the client (after Firebase is mocked) + +# It is safer to use a fixture for the client to ensure fresh state, +# but global is okay for simple tests if the app is stateless. client = TestClient(app) @@ -126,8 +131,7 @@ class MockGeminiResponse: ) assert response.status_code == 200 - - assert response.json() == {"tailored_resume": "This is a fake tailored resume."} + assert response.json().get("tailored_resume") == "This is a fake tailored resume." def test_fixture_is_working(sample_job_data):