feature: create multitasking
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
"""
|
||||
Tests for jobs and events endpoints.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from cpv3.modules.jobs.models import Job, JobEvent
|
||||
from cpv3.modules.users.models import User
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_job(test_db_session: AsyncSession, test_user: User) -> Job:
|
||||
"""Create a test job owned by test_user."""
|
||||
job = Job(
|
||||
id=uuid.uuid4(),
|
||||
broker_id="test-broker-123",
|
||||
user_id=test_user.id,
|
||||
status="PENDING",
|
||||
job_type="PENDING",
|
||||
is_active=True,
|
||||
)
|
||||
test_db_session.add(job)
|
||||
await test_db_session.commit()
|
||||
await test_db_session.refresh(job)
|
||||
return job
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def other_job(test_db_session: AsyncSession, other_user: User) -> Job:
|
||||
"""Create a job owned by another user."""
|
||||
job = Job(
|
||||
id=uuid.uuid4(),
|
||||
broker_id="other-broker-456",
|
||||
user_id=other_user.id,
|
||||
status="RUNNING",
|
||||
job_type="RUNNING",
|
||||
is_active=True,
|
||||
)
|
||||
test_db_session.add(job)
|
||||
await test_db_session.commit()
|
||||
await test_db_session.refresh(job)
|
||||
return job
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_event(test_db_session: AsyncSession, test_job: Job) -> JobEvent:
|
||||
"""Create a test job event linked to test_job."""
|
||||
event = JobEvent(
|
||||
id=uuid.uuid4(),
|
||||
job_id=test_job.id,
|
||||
event_type="started",
|
||||
payload={"message": "Job started"},
|
||||
is_active=True,
|
||||
)
|
||||
test_db_session.add(event)
|
||||
await test_db_session.commit()
|
||||
await test_db_session.refresh(event)
|
||||
return event
|
||||
|
||||
|
||||
class TestListJobsEndpoint:
|
||||
"""Tests for GET /api/jobs/jobs/."""
|
||||
|
||||
async def test_list_jobs(self, auth_client: AsyncClient, test_job: Job):
|
||||
"""Test listing jobs."""
|
||||
response = await auth_client.get("/api/jobs/jobs/")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
async def test_list_jobs_unauthenticated(self, async_client: AsyncClient):
|
||||
"""Test listing jobs without auth returns 401."""
|
||||
response = await async_client.get("/api/jobs/jobs/")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestCreateJobEndpoint:
|
||||
"""Tests for POST /api/jobs/jobs/."""
|
||||
|
||||
async def test_create_job_success(self, auth_client: AsyncClient):
|
||||
"""Test creating a job."""
|
||||
response = await auth_client.post(
|
||||
"/api/jobs/jobs/",
|
||||
json={
|
||||
"broker_id": "new-broker-789",
|
||||
"status": "PENDING",
|
||||
"job_type": "PENDING",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["broker_id"] == "new-broker-789"
|
||||
assert data["status"] == "PENDING"
|
||||
|
||||
async def test_create_job_with_input_data(self, auth_client: AsyncClient):
|
||||
"""Test creating a job with input data."""
|
||||
response = await auth_client.post(
|
||||
"/api/jobs/jobs/",
|
||||
json={
|
||||
"broker_id": "broker-with-data",
|
||||
"input_data": {"file_path": "uploads/test.mp4"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["input_data"]["file_path"] == "uploads/test.mp4"
|
||||
|
||||
async def test_create_job_unauthenticated(self, async_client: AsyncClient):
|
||||
"""Test creating job without auth returns 401."""
|
||||
response = await async_client.post(
|
||||
"/api/jobs/jobs/",
|
||||
json={"broker_id": "test"},
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestRetrieveJobEndpoint:
|
||||
"""Tests for GET /api/jobs/jobs/{job_id}/."""
|
||||
|
||||
async def test_retrieve_own_job(self, auth_client: AsyncClient, test_job: Job):
|
||||
"""Test retrieving own job."""
|
||||
response = await auth_client.get(f"/api/jobs/jobs/{test_job.id}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == str(test_job.id)
|
||||
assert data["broker_id"] == test_job.broker_id
|
||||
|
||||
async def test_retrieve_other_job_as_staff(
|
||||
self, staff_client: AsyncClient, test_job: Job
|
||||
):
|
||||
"""Test staff can retrieve any job."""
|
||||
response = await staff_client.get(f"/api/jobs/jobs/{test_job.id}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_retrieve_nonexistent_job(self, auth_client: AsyncClient):
|
||||
"""Test retrieving nonexistent job returns 404."""
|
||||
fake_id = uuid.uuid4()
|
||||
response = await auth_client.get(f"/api/jobs/jobs/{fake_id}/")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
async def test_retrieve_other_job_forbidden(
|
||||
self, auth_client: AsyncClient, other_job: Job
|
||||
):
|
||||
"""Test regular user cannot retrieve other user's job."""
|
||||
response = await auth_client.get(f"/api/jobs/jobs/{other_job.id}/")
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
class TestPatchJobEndpoint:
|
||||
"""Tests for PATCH /api/jobs/jobs/{job_id}/."""
|
||||
|
||||
async def test_patch_own_job(self, auth_client: AsyncClient, test_job: Job):
|
||||
"""Test updating own job."""
|
||||
response = await auth_client.patch(
|
||||
f"/api/jobs/jobs/{test_job.id}/",
|
||||
json={"status": "RUNNING", "current_message": "Processing..."},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "RUNNING"
|
||||
assert data["current_message"] == "Processing..."
|
||||
|
||||
async def test_patch_job_progress(self, auth_client: AsyncClient, test_job: Job):
|
||||
"""Test updating job progress."""
|
||||
response = await auth_client.patch(
|
||||
f"/api/jobs/jobs/{test_job.id}/",
|
||||
json={"project_pct": 50.0},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["project_pct"] == 50.0
|
||||
|
||||
async def test_patch_other_job_as_staff(
|
||||
self, staff_client: AsyncClient, test_job: Job
|
||||
):
|
||||
"""Test staff can update any job."""
|
||||
response = await staff_client.patch(
|
||||
f"/api/jobs/jobs/{test_job.id}/",
|
||||
json={"status": "DONE"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_patch_nonexistent_job(self, auth_client: AsyncClient):
|
||||
"""Test patching nonexistent job returns 404."""
|
||||
fake_id = uuid.uuid4()
|
||||
response = await auth_client.patch(
|
||||
f"/api/jobs/jobs/{fake_id}/",
|
||||
json={"status": "DONE"},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
async def test_patch_other_job_forbidden(
|
||||
self, auth_client: AsyncClient, other_job: Job
|
||||
):
|
||||
"""Test regular user cannot update other user's job."""
|
||||
response = await auth_client.patch(
|
||||
f"/api/jobs/jobs/{other_job.id}/",
|
||||
json={"status": "CANCELLED"},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
class TestDeleteJobEndpoint:
|
||||
"""Tests for DELETE /api/jobs/jobs/{job_id}/."""
|
||||
|
||||
async def test_delete_own_job(self, auth_client: AsyncClient, test_job: Job):
|
||||
"""Test deleting own job."""
|
||||
response = await auth_client.delete(f"/api/jobs/jobs/{test_job.id}/")
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
async def test_delete_other_job_as_staff(
|
||||
self, staff_client: AsyncClient, test_job: Job
|
||||
):
|
||||
"""Test staff can delete any job."""
|
||||
response = await staff_client.delete(f"/api/jobs/jobs/{test_job.id}/")
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
async def test_delete_nonexistent_job(self, auth_client: AsyncClient):
|
||||
"""Test deleting nonexistent job returns 404."""
|
||||
fake_id = uuid.uuid4()
|
||||
response = await auth_client.delete(f"/api/jobs/jobs/{fake_id}/")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
async def test_delete_other_job_forbidden(
|
||||
self, auth_client: AsyncClient, other_job: Job
|
||||
):
|
||||
"""Test regular user cannot delete other user's job."""
|
||||
response = await auth_client.delete(f"/api/jobs/jobs/{other_job.id}/")
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
class TestListEventsEndpoint:
|
||||
"""Tests for GET /api/jobs/events/."""
|
||||
|
||||
async def test_list_events(self, auth_client: AsyncClient, test_event: JobEvent):
|
||||
"""Test listing job events."""
|
||||
response = await auth_client.get("/api/jobs/events/")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
async def test_list_events_unauthenticated(self, async_client: AsyncClient):
|
||||
"""Test listing events without auth returns 401."""
|
||||
response = await async_client.get("/api/jobs/events/")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestCreateEventEndpoint:
|
||||
"""Tests for POST /api/jobs/events/."""
|
||||
|
||||
async def test_create_event_success(self, auth_client: AsyncClient, test_job: Job):
|
||||
"""Test creating a job event."""
|
||||
response = await auth_client.post(
|
||||
"/api/jobs/events/",
|
||||
json={
|
||||
"job_id": str(test_job.id),
|
||||
"event_type": "progress",
|
||||
"payload": {"percentage": 25},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["event_type"] == "progress"
|
||||
assert data["payload"]["percentage"] == 25
|
||||
|
||||
async def test_create_event_unauthenticated(self, async_client: AsyncClient):
|
||||
"""Test creating event without auth returns 401."""
|
||||
response = await async_client.post(
|
||||
"/api/jobs/events/",
|
||||
json={
|
||||
"job_id": str(uuid.uuid4()),
|
||||
"event_type": "test",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestRetrieveEventEndpoint:
|
||||
"""Tests for GET /api/jobs/events/{event_id}/."""
|
||||
|
||||
async def test_retrieve_event(self, auth_client: AsyncClient, test_event: JobEvent):
|
||||
"""Test retrieving a job event."""
|
||||
response = await auth_client.get(f"/api/jobs/events/{test_event.id}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == str(test_event.id)
|
||||
assert data["event_type"] == test_event.event_type
|
||||
|
||||
async def test_retrieve_nonexistent_event(self, auth_client: AsyncClient):
|
||||
"""Test retrieving nonexistent event returns 404."""
|
||||
fake_id = uuid.uuid4()
|
||||
response = await auth_client.get(f"/api/jobs/events/{fake_id}/")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestPatchEventEndpoint:
|
||||
"""Tests for PATCH /api/jobs/events/{event_id}/."""
|
||||
|
||||
async def test_patch_event(self, auth_client: AsyncClient, test_event: JobEvent):
|
||||
"""Test updating a job event."""
|
||||
response = await auth_client.patch(
|
||||
f"/api/jobs/events/{test_event.id}/",
|
||||
json={"payload": {"updated": True}},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["payload"]["updated"] is True
|
||||
|
||||
async def test_patch_nonexistent_event(self, auth_client: AsyncClient):
|
||||
"""Test patching nonexistent event returns 404."""
|
||||
fake_id = uuid.uuid4()
|
||||
response = await auth_client.patch(
|
||||
f"/api/jobs/events/{fake_id}/",
|
||||
json={"payload": {}},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestDeleteEventEndpoint:
|
||||
"""Tests for DELETE /api/jobs/events/{event_id}/."""
|
||||
|
||||
async def test_delete_event(self, auth_client: AsyncClient, test_event: JobEvent):
|
||||
"""Test deleting a job event."""
|
||||
response = await auth_client.delete(f"/api/jobs/events/{test_event.id}/")
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
async def test_delete_nonexistent_event(self, auth_client: AsyncClient):
|
||||
"""Test deleting nonexistent event returns 404."""
|
||||
fake_id = uuid.uuid4()
|
||||
response = await auth_client.delete(f"/api/jobs/events/{fake_id}/")
|
||||
|
||||
assert response.status_code == 404
|
||||
Reference in New Issue
Block a user