from __future__ import annotations from datetime import datetime, timezone from uuid import UUID, uuid4 from fastapi.testclient import TestClient from app import app from core.auth.models import CurrentUser from v1.automation_jobs.dependencies import get_automation_jobs_service from v1.automation_jobs.service import ( AutomationJobLimitExceeded, AutomationJobNotFound, ) from v1.automation_jobs.schemas import ( AutomationJobCreateRequest, AutomationJobListResponse, AutomationJobResponse, AutomationJobUpdateRequest, ) from v1.users.dependencies import get_current_user def _make_job_response( job_id: UUID | None = None, owner_id: UUID | None = None, **overrides ) -> AutomationJobResponse: now = datetime.now(timezone.utc) return AutomationJobResponse( id=job_id or uuid4(), owner_id=owner_id or uuid4(), title=overrides.get("title", "Test Job"), timezone=overrides.get("timezone", "Asia/Shanghai"), status=overrides.get("status", "active"), is_system=overrides.get("is_system", False), config=overrides.get( "config", { "input_template": "Hello", "enabled_skills": [], "context": { "source": "latest_chat", "window_mode": "day", "window_count": 2, }, "schedule": { "type": "daily", "run_at": {"hour": 9, "minute": 0}, }, }, ), next_run_at=overrides.get("next_run_at", now), created_at=overrides.get("created_at", now), updated_at=overrides.get("updated_at", now), ) def test_list_automation_jobs_requires_auth() -> None: client = TestClient(app) response = client.get("/api/v1/automation-jobs") assert response.status_code == 401 def test_list_automation_jobs_returns_empty_when_no_jobs() -> None: class FakeService: async def list_by_owner(self, *, owner_id: UUID) -> AutomationJobListResponse: return AutomationJobListResponse(items=[]) app.dependency_overrides[get_automation_jobs_service] = lambda: FakeService() app.dependency_overrides.pop(get_current_user, None) client = TestClient(app) try: response = client.get("/api/v1/automation-jobs") assert response.status_code == 401 finally: app.dependency_overrides = {} def test_list_automation_jobs_returns_jobs() -> None: user_id = uuid4() job = _make_job_response(owner_id=user_id) class FakeService: async def list_by_owner(self, *, owner_id: UUID) -> AutomationJobListResponse: if owner_id == user_id: return AutomationJobListResponse(items=[job]) return AutomationJobListResponse(items=[]) app.dependency_overrides[get_automation_jobs_service] = lambda: FakeService() app.dependency_overrides[get_current_user] = lambda: CurrentUser( id=user_id, phone="+8613812345678" ) client = TestClient(app) try: response = client.get("/api/v1/automation-jobs") assert response.status_code == 200 data = response.json() assert len(data["items"]) == 1 assert data["items"][0]["title"] == "Test Job" finally: app.dependency_overrides = {} def test_create_automation_job_requires_auth() -> None: class FakeService: pass app.dependency_overrides[get_automation_jobs_service] = lambda: FakeService() app.dependency_overrides.pop(get_current_user, None) client = TestClient(app) try: response = client.post( "/api/v1/automation-jobs", json={ "title": "New Job", "timezone": "Asia/Shanghai", "config": { "input_template": "Hello", "enabled_skills": [], "context": { "source": "latest_chat", "window_mode": "day", "window_count": 2, }, "schedule": { "type": "daily", "run_at": {"hour": 9, "minute": 0}, }, }, }, ) assert response.status_code == 401 finally: app.dependency_overrides = {} def test_create_automation_job_succeeds() -> None: user_id = uuid4() new_job = _make_job_response(owner_id=user_id, title="New Job") class FakeService: async def create( self, *, owner_id: UUID, data: AutomationJobCreateRequest ) -> AutomationJobResponse: return new_job app.dependency_overrides[get_automation_jobs_service] = lambda: FakeService() app.dependency_overrides[get_current_user] = lambda: CurrentUser( id=user_id, phone="+8613812345678" ) client = TestClient(app) try: response = client.post( "/api/v1/automation-jobs", json={ "title": "New Job", "timezone": "Asia/Shanghai", "status": "active", "config": { "input_template": "Hello", "enabled_skills": [], "context": { "source": "latest_chat", "window_mode": "day", "window_count": 2, }, "schedule": { "type": "daily", "run_at": {"hour": 9, "minute": 0}, }, }, }, ) assert response.status_code == 201 data = response.json() assert data["title"] == "New Job" finally: app.dependency_overrides = {} def test_create_automation_job_respects_limit() -> None: user_id = uuid4() class FakeService: async def create( self, *, owner_id: UUID, data: AutomationJobCreateRequest ) -> AutomationJobResponse: raise AutomationJobLimitExceeded() app.dependency_overrides[get_automation_jobs_service] = lambda: FakeService() app.dependency_overrides[get_current_user] = lambda: CurrentUser( id=user_id, phone="+8613812345678" ) client = TestClient(app) try: response = client.post( "/api/v1/automation-jobs", json={ "title": "New Job", "timezone": "Asia/Shanghai", "status": "active", "config": { "input_template": "Hello", "enabled_skills": [], "context": { "source": "latest_chat", "window_mode": "day", "window_count": 2, }, "schedule": { "type": "daily", "run_at": {"hour": 9, "minute": 0}, }, }, }, ) assert response.status_code == 400 assert "maximum" in response.json()["detail"].lower() finally: app.dependency_overrides = {} def test_get_automation_job_requires_auth() -> None: client = TestClient(app) response = client.get(f"/api/v1/automation-jobs/{uuid4()}") assert response.status_code == 401 def test_get_automation_job_returns_job() -> None: user_id = uuid4() job_id = uuid4() job = _make_job_response(job_id=job_id, owner_id=user_id) captured_job_id = job_id captured_owner_id = user_id class FakeService: async def get_by_id( self, *, job_id: UUID, owner_id: UUID ) -> AutomationJobResponse: if job_id == captured_job_id and owner_id == captured_owner_id: return job raise AutomationJobNotFound() app.dependency_overrides[get_automation_jobs_service] = lambda: FakeService() app.dependency_overrides[get_current_user] = lambda: CurrentUser( id=user_id, phone="+8613812345678" ) client = TestClient(app) try: response = client.get(f"/api/v1/automation-jobs/{job_id}") assert response.status_code == 200 data = response.json() assert data["title"] == "Test Job" finally: app.dependency_overrides = {} def test_get_automation_job_returns_404_when_not_found() -> None: user_id = uuid4() class FakeService: async def get_by_id( self, *, job_id: UUID, owner_id: UUID ) -> AutomationJobResponse: raise AutomationJobNotFound() app.dependency_overrides[get_automation_jobs_service] = lambda: FakeService() app.dependency_overrides[get_current_user] = lambda: CurrentUser( id=user_id, phone="+8613812345678" ) client = TestClient(app) try: response = client.get(f"/api/v1/automation-jobs/{uuid4()}") assert response.status_code == 404 finally: app.dependency_overrides = {} def test_update_automation_job_requires_auth() -> None: client = TestClient(app) response = client.patch( f"/api/v1/automation-jobs/{uuid4()}", json={"title": "Updated"}, ) assert response.status_code == 401 def test_update_automation_job_succeeds() -> None: user_id = uuid4() job_id = uuid4() updated_job = _make_job_response( job_id=job_id, owner_id=user_id, title="Updated Title", ) class FakeService: async def update( self, *, job_id: UUID, owner_id: UUID, data: AutomationJobUpdateRequest, ) -> AutomationJobResponse: return updated_job app.dependency_overrides[get_automation_jobs_service] = lambda: FakeService() app.dependency_overrides[get_current_user] = lambda: CurrentUser( id=user_id, phone="+8613812345678" ) client = TestClient(app) try: response = client.patch( f"/api/v1/automation-jobs/{job_id}", json={"title": "Updated Title"}, ) assert response.status_code == 200 data = response.json() assert data["title"] == "Updated Title" finally: app.dependency_overrides = {} def test_update_automation_job_returns_404_when_not_found() -> None: user_id = uuid4() class FakeService: async def update( self, *, job_id: UUID, owner_id: UUID, data: AutomationJobUpdateRequest, ) -> AutomationJobResponse: raise AutomationJobNotFound() app.dependency_overrides[get_automation_jobs_service] = lambda: FakeService() app.dependency_overrides[get_current_user] = lambda: CurrentUser( id=user_id, phone="+8613812345678" ) client = TestClient(app) try: response = client.patch( f"/api/v1/automation-jobs/{uuid4()}", json={"title": "Updated"} ) assert response.status_code == 404 finally: app.dependency_overrides = {} def test_delete_automation_job_requires_auth() -> None: client = TestClient(app) response = client.delete(f"/api/v1/automation-jobs/{uuid4()}") assert response.status_code == 401 def test_delete_automation_job_succeeds() -> None: user_id = uuid4() job_id = uuid4() class FakeService: async def delete(self, *, job_id: UUID, owner_id: UUID) -> None: pass app.dependency_overrides[get_automation_jobs_service] = lambda: FakeService() app.dependency_overrides[get_current_user] = lambda: CurrentUser( id=user_id, phone="+8613812345678" ) client = TestClient(app) try: response = client.delete(f"/api/v1/automation-jobs/{job_id}") assert response.status_code == 204 finally: app.dependency_overrides = {} def test_delete_automation_job_returns_404_when_not_found() -> None: user_id = uuid4() class FakeService: async def delete(self, *, job_id: UUID, owner_id: UUID) -> None: raise AutomationJobNotFound() app.dependency_overrides[get_automation_jobs_service] = lambda: FakeService() app.dependency_overrides[get_current_user] = lambda: CurrentUser( id=user_id, phone="+8613812345678" ) client = TestClient(app) try: response = client.delete(f"/api/v1/automation-jobs/{uuid4()}") assert response.status_code == 404 finally: app.dependency_overrides = {}