From a2bc84205c3369ea9a85d253bccf931c4d478b48 Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Fri, 25 Sep 2026 13:24:50 +0530 Subject: [PATCH 1/4] fix(ci): fix pip cache dependency path, package init, and test execution path --- .github/workflows/ci.yml | 8 +++----- backend/__init__.py | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 backend/__init__.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b67bb8e..fc850fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,19 +19,17 @@ jobs: with: python-version: "3.12" cache: "pip" + cache-dependency-path: "backend/requirements.txt" - name: Install backend dependencies run: | - cd backend python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pytest + pip install -r backend/requirements.txt - name: Run backend tests run: | - cd backend export PYTHONPATH=. - pytest + pytest backend/ env: AWS_REGION: ap-south-1 AWS_ACCESS_KEY_ID: mock_access_key diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..91b66c4 --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1 @@ +"""SmallOps backend package.""" From 351e2181a0b6dab79d74b7343f49fb4b1dd48690 Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Fri, 25 Sep 2026 13:37:16 +0530 Subject: [PATCH 2/4] fix(tests): prevent global boto3 module pollution and update test assertions --- backend/api/routes_share.py | 4 ++-- backend/tests/test_auth.py | 3 +-- backend/tests/test_deploy.py | 4 ++-- backend/tests/test_maintenance_orchestrator.py | 13 +++++++------ backend/tests/test_routes_maintenance.py | 11 ++++++----- backend/tests/test_safety_guards.py | 13 +++++++------ backend/tests/test_share.py | 1 + 7 files changed, 26 insertions(+), 23 deletions(-) diff --git a/backend/api/routes_share.py b/backend/api/routes_share.py index c5a7ecb..a3af185 100644 --- a/backend/api/routes_share.py +++ b/backend/api/routes_share.py @@ -82,8 +82,8 @@ def send_invite_email( return response -@router.post("/apps/{app_id}/invite") -@router.post("/share/{app_id}/invite") +@router.post("/apps/{app_id}/invite", dependencies=[Depends(require_editor)]) +@router.post("/share/{app_id}/invite", dependencies=[Depends(require_editor)]) async def invite_user(app_id: str, body: InviteRequest): """Invite a collaborator by email with a Viewer or Editor role.""" settings = get_settings() diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index f1acb57..c2576b1 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -170,8 +170,7 @@ def test_deploy_editor_returns_200( assert resp.status_code == 200 data = resp.json() assert data["app_id"] == "test-app-1" - assert data["status"] == "deployed" - assert data["live_url"] == "https://example.lambda-url.ap-south-1.on.aws/" + assert data["status"] in ("building", "deployed") @patch("backend.auth.roles.verify_cognito_token") def test_revert_viewer_returns_403(self, mock_verify, client): diff --git a/backend/tests/test_deploy.py b/backend/tests/test_deploy.py index 1ad78a7..836bfbd 100644 --- a/backend/tests/test_deploy.py +++ b/backend/tests/test_deploy.py @@ -29,10 +29,10 @@ def test_creates_valid_zip(self): buf = io.BytesIO(zip_bytes) with zipfile.ZipFile(buf, "r") as zf: names = zf.namelist() - assert "app_code.py" in names + assert "index.html" in names assert "lambda_function.py" in names # Verify the app code content - assert zf.read("app_code.py").decode() == SAMPLE_CODE + assert zf.read("index.html").decode() == SAMPLE_CODE def test_handler_is_valid_python(self): zip_bytes = _package_code(SAMPLE_CODE) diff --git a/backend/tests/test_maintenance_orchestrator.py b/backend/tests/test_maintenance_orchestrator.py index 55a2b18..faaffda 100644 --- a/backend/tests/test_maintenance_orchestrator.py +++ b/backend/tests/test_maintenance_orchestrator.py @@ -5,12 +5,13 @@ import sys from unittest.mock import AsyncMock, MagicMock, patch -# Ensure boto3 and submodule mocks exist in test environments -mock_boto3 = MagicMock() -mock_boto3.__path__ = [] -sys.modules["boto3"] = mock_boto3 -sys.modules["boto3.dynamodb"] = MagicMock() -sys.modules["boto3.dynamodb.conditions"] = MagicMock() +# Ensure boto3 and submodule mocks exist in test environments if missing +if "boto3" not in sys.modules: + mock_boto3 = MagicMock() + mock_boto3.__path__ = [] + sys.modules["boto3"] = mock_boto3 + sys.modules["boto3.dynamodb"] = MagicMock() + sys.modules["boto3.dynamodb.conditions"] = MagicMock() if "botocore" not in sys.modules: sys.modules["botocore"] = MagicMock() diff --git a/backend/tests/test_routes_maintenance.py b/backend/tests/test_routes_maintenance.py index fbbd1d5..64740df 100644 --- a/backend/tests/test_routes_maintenance.py +++ b/backend/tests/test_routes_maintenance.py @@ -6,11 +6,12 @@ from unittest.mock import AsyncMock, MagicMock, patch # Ensure mocks for boto3, jose, and structlog if missing in test environment -mock_boto3 = MagicMock() -mock_boto3.__path__ = [] -sys.modules["boto3"] = mock_boto3 -sys.modules["boto3.dynamodb"] = MagicMock() -sys.modules["boto3.dynamodb.conditions"] = MagicMock() +if "boto3" not in sys.modules: + mock_boto3 = MagicMock() + mock_boto3.__path__ = [] + sys.modules["boto3"] = mock_boto3 + sys.modules["boto3.dynamodb"] = MagicMock() + sys.modules["boto3.dynamodb.conditions"] = MagicMock() if "botocore" not in sys.modules: sys.modules["botocore"] = MagicMock() diff --git a/backend/tests/test_safety_guards.py b/backend/tests/test_safety_guards.py index ac874a3..57607e4 100644 --- a/backend/tests/test_safety_guards.py +++ b/backend/tests/test_safety_guards.py @@ -5,12 +5,13 @@ import sys from unittest.mock import AsyncMock, MagicMock, patch -# Ensure boto3 mocks for test environments -mock_boto3 = MagicMock() -mock_boto3.__path__ = [] -sys.modules["boto3"] = mock_boto3 -sys.modules["boto3.dynamodb"] = MagicMock() -sys.modules["boto3.dynamodb.conditions"] = MagicMock() +# Ensure boto3 mocks for test environments if missing +if "boto3" not in sys.modules: + mock_boto3 = MagicMock() + mock_boto3.__path__ = [] + sys.modules["boto3"] = mock_boto3 + sys.modules["boto3.dynamodb"] = MagicMock() + sys.modules["boto3.dynamodb.conditions"] = MagicMock() if "botocore" not in sys.modules: sys.modules["botocore"] = MagicMock() diff --git a/backend/tests/test_share.py b/backend/tests/test_share.py index 1907e02..0e4cf80 100644 --- a/backend/tests/test_share.py +++ b/backend/tests/test_share.py @@ -29,6 +29,7 @@ def test_send_email_params_with_moto(self): with mock_aws(): ses = boto3.client("ses", region_name=TEST_REGION) ses.verify_email_identity(EmailAddress=SENDER_EMAIL) + ses.verify_email_identity(EmailAddress=RECIPIENT_EMAIL) resp = send_invite_email( to_email=RECIPIENT_EMAIL, From 27aa5535516046fe3f5e9ca8a01a2533c88edcd9 Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Fri, 25 Sep 2026 13:45:37 +0530 Subject: [PATCH 3/4] fix(security): enforce require_editor on deploy and revert routes, update e2e test assertion --- backend/api/routes_deploy.py | 5 +++-- backend/api/routes_timeline.py | 2 +- backend/tests/test_e2e_smoke.py | 4 +--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/api/routes_deploy.py b/backend/api/routes_deploy.py index edc8591..27ccf37 100644 --- a/backend/api/routes_deploy.py +++ b/backend/api/routes_deploy.py @@ -4,11 +4,12 @@ import logging -from fastapi import APIRouter, HTTPException, BackgroundTasks +from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends from pydantic import BaseModel from backend.agent.planner import plan_and_execute from backend.agent.trace_logger import log_steps +from backend.auth.roles import require_editor from backend.config import get_settings from backend.deploy.lambda_deployer import deploy_to_lambda from backend.models.app import App, StepType, TimelineStep @@ -100,7 +101,7 @@ def _update_app_status(app_id: str, body: DeployRequest, status: str, function_u ) -@router.post("/{app_id}") +@router.post("/{app_id}", dependencies=[Depends(require_editor)]) async def deploy_app(app_id: str, body: DeployRequest, background_tasks: BackgroundTasks): """Start the deploy pipeline in the background and return immediately.""" diff --git a/backend/api/routes_timeline.py b/backend/api/routes_timeline.py index 1ef4cf6..a794439 100644 --- a/backend/api/routes_timeline.py +++ b/backend/api/routes_timeline.py @@ -41,7 +41,7 @@ async def get_timeline_step(app_id: str, step_id: str): return step.model_dump() -@router.post("/apps/{app_id}/revert/{step_id}") +@router.post("/apps/{app_id}/revert/{step_id}", dependencies=[Depends(require_editor)]) async def revert_to_step(app_id: str, step_id: str): """Revert the live app to the code snapshot at a given timeline step. diff --git a/backend/tests/test_e2e_smoke.py b/backend/tests/test_e2e_smoke.py index d9bcb43..bb77c20 100644 --- a/backend/tests/test_e2e_smoke.py +++ b/backend/tests/test_e2e_smoke.py @@ -179,9 +179,7 @@ def test_full_pipeline_clarify_deploy_timeline_edit_revert_share( assert deploy_resp.status_code == 200 deploy_data = deploy_resp.json() assert deploy_data["app_id"] == app_id - assert deploy_data["status"] == "deployed" - assert deploy_data["live_url"] == live_url - assert deploy_data["steps_logged"] >= 3 + assert deploy_data["status"] in ("building", "deployed") # ── Step 3: Inspect Decision Timeline ──────────────────────────── timeline_resp = client.get(f"/apps/{app_id}/timeline") From a4def36b5d643fb294b7e69d153cbed96d8bada5 Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Mon, 28 Sep 2026 23:07:23 +0530 Subject: [PATCH 4/4] chore(git): ignore build_lambda/ and zip files in .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 4c2ad28..6b31885 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ __pycache__/ *.egg dist/ build/ +build_lambda/ +*.zip .venv/ venv/ .pytest_cache/