feat: 添加自动化任务(automation_jobs)功能模块

This commit is contained in:
qzl
2026-03-24 12:38:11 +08:00
parent f4b7eb7e09
commit 23359c2d01
43 changed files with 4266 additions and 1139 deletions
@@ -0,0 +1,371 @@
from __future__ import annotations
from datetime import datetime, time, 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"),
schedule_type=overrides.get("schedule_type", "daily"),
run_at=overrides.get("run_at", time(9, 0, 0)),
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_tools": [], "context": {}}
),
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",
"schedule_type": "daily",
"run_at": "09:00:00",
"timezone": "Asia/Shanghai",
"config": {
"input_template": "Hello",
"enabled_tools": [],
"context": {},
},
},
)
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",
"schedule_type": "daily",
"run_at": "09:00:00",
"timezone": "Asia/Shanghai",
"status": "active",
"config": {
"input_template": "Hello",
"enabled_tools": [],
"context": {},
},
},
)
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",
"schedule_type": "daily",
"run_at": "09:00:00",
"timezone": "Asia/Shanghai",
"status": "active",
"config": {
"input_template": "Hello",
"enabled_tools": [],
"context": {},
},
},
)
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(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(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 = {}