feature: create multitasking

This commit is contained in:
Daniil
2026-02-04 02:19:50 +03:00
parent 67e0f22b4f
commit a25bf623ea
24 changed files with 5227 additions and 21 deletions
+159
View File
@@ -0,0 +1,159 @@
"""
Tests for authentication endpoints.
"""
from __future__ import annotations
from datetime import timedelta
import pytest
from httpx import AsyncClient
from cpv3.infrastructure.security import create_token
from cpv3.modules.users.models import User
class TestRegisterEndpoint:
"""Tests for POST /auth/register."""
async def test_register_success(self, async_client: AsyncClient):
"""Test successful user registration."""
response = await async_client.post(
"/auth/register",
json={
"username": "newuser",
"email": "newuser@example.com",
"password": "securepassword123",
"first_name": "New",
"last_name": "User",
},
)
assert response.status_code == 201
data = response.json()
assert "access" in data
assert "refresh" in data
assert data["user"]["username"] == "newuser"
assert data["user"]["email"] == "newuser@example.com"
async def test_register_duplicate_username(
self, async_client: AsyncClient, test_user: User
):
"""Test registration fails with duplicate username."""
response = await async_client.post(
"/auth/register",
json={
"username": test_user.username, # existing username
"email": "another@example.com",
"password": "password123",
},
)
assert response.status_code == 400
async def test_register_missing_required_fields(self, async_client: AsyncClient):
"""Test registration fails with missing required fields."""
response = await async_client.post(
"/auth/register",
json={"username": "someuser"}, # missing email and password
)
assert response.status_code == 422
class TestLoginEndpoint:
"""Tests for POST /auth/login."""
async def test_login_success(self, async_client: AsyncClient, test_user: User):
"""Test successful login."""
response = await async_client.post(
"/auth/login",
json={
"username": "testuser",
"password": "testpassword",
},
)
assert response.status_code == 200
data = response.json()
assert "access" in data
assert "refresh" in data
assert data["user"]["username"] == "testuser"
async def test_login_invalid_password(
self, async_client: AsyncClient, test_user: User
):
"""Test login fails with wrong password."""
response = await async_client.post(
"/auth/login",
json={
"username": "testuser",
"password": "wrongpassword",
},
)
assert response.status_code == 401
assert response.json()["detail"] == "Invalid credentials"
async def test_login_nonexistent_user(self, async_client: AsyncClient):
"""Test login fails for nonexistent user."""
response = await async_client.post(
"/auth/login",
json={
"username": "nonexistent",
"password": "password123",
},
)
assert response.status_code == 401
assert response.json()["detail"] == "Invalid credentials"
class TestRefreshEndpoint:
"""Tests for POST /auth/refresh."""
async def test_refresh_success(self, async_client: AsyncClient, test_user: User):
"""Test successful token refresh."""
refresh_token = create_token(
subject=str(test_user.id),
token_type="refresh",
expires_in=timedelta(days=7),
)
response = await async_client.post(
"/auth/refresh",
json={"refresh": refresh_token},
)
assert response.status_code == 200
data = response.json()
assert "access" in data
assert "refresh" in data
async def test_refresh_with_access_token_fails(
self, async_client: AsyncClient, test_user: User
):
"""Test refresh fails when using access token instead of refresh token."""
access_token = create_token(
subject=str(test_user.id),
token_type="access",
expires_in=timedelta(minutes=15),
)
response = await async_client.post(
"/auth/refresh",
json={"refresh": access_token},
)
assert response.status_code == 401
assert response.json()["detail"] == "Invalid refresh token"
async def test_refresh_with_invalid_token(self, async_client: AsyncClient):
"""Test refresh fails with invalid token."""
response = await async_client.post(
"/auth/refresh",
json={"refresh": "invalid.token.here"},
)
assert response.status_code == 401
assert response.json()["detail"] == "Invalid refresh token"
@@ -0,0 +1,80 @@
"""
Tests for captions endpoints.
"""
from __future__ import annotations
from unittest.mock import AsyncMock, patch
import pytest
from httpx import AsyncClient
class TestGetVideoEndpoint:
"""Tests for POST /api/captions/get_video/."""
async def test_get_video_success(self, auth_client: AsyncClient):
"""Test caption burn-in endpoint returns result."""
mock_transcription = {
"segments": [
{
"text": "Hello world",
"semantic_tags": [],
"structure_tags": [],
"time": {"start": 0.0, "end": 2.0},
"lines": [
{
"text": "Hello world",
"semantic_tags": [],
"structure_tags": [],
"time": {"start": 0.0, "end": 2.0},
"words": [],
}
],
}
]
}
with patch(
"cpv3.modules.captions.router.generate_captions",
new_callable=AsyncMock,
return_value="uploads/output/captioned_video.mp4",
):
response = await auth_client.post(
"/api/captions/get_video/",
json={
"folder": "output",
"video_s3_path": "uploads/source_video.mp4",
"transcription": mock_transcription,
},
)
assert response.status_code == 200
data = response.json()
assert "result" in data
assert data["result"] == "uploads/output/captioned_video.mp4"
async def test_get_video_unauthenticated(self, async_client: AsyncClient):
"""Test caption burn-in without auth returns 401."""
response = await async_client.post(
"/api/captions/get_video/",
json={
"folder": "output",
"video_s3_path": "test.mp4",
"transcription": {"segments": []},
},
)
assert response.status_code == 401
async def test_get_video_missing_fields(self, auth_client: AsyncClient):
"""Test caption burn-in with missing required fields returns 422."""
response = await auth_client.post(
"/api/captions/get_video/",
json={
"folder": "output",
# missing video_s3_path and transcription
},
)
assert response.status_code == 422
+293
View File
@@ -0,0 +1,293 @@
"""
Tests for file management endpoints.
"""
from __future__ import annotations
import io
import uuid
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.modules.files.models import File
from cpv3.modules.users.models import User
@pytest.fixture
async def test_file(test_db_session: AsyncSession, test_user: User) -> File:
"""Create a test file entry owned by test_user."""
file = File(
id=uuid.uuid4(),
owner_id=test_user.id,
original_filename="test-document.pdf",
path="uploads/test-document.pdf",
storage_backend="LOCAL",
mime_type="application/pdf",
size_bytes=1024,
is_uploaded=True,
is_active=True,
)
test_db_session.add(file)
await test_db_session.commit()
await test_db_session.refresh(file)
return file
@pytest.fixture
async def other_file(test_db_session: AsyncSession, other_user: User) -> File:
"""Create a file entry owned by another user."""
file = File(
id=uuid.uuid4(),
owner_id=other_user.id,
original_filename="other-document.pdf",
path="uploads/other-document.pdf",
storage_backend="LOCAL",
mime_type="application/pdf",
size_bytes=2048,
is_uploaded=True,
is_active=True,
)
test_db_session.add(file)
await test_db_session.commit()
await test_db_session.refresh(file)
return file
class TestUploadFileEndpoint:
"""Tests for POST /api/files/upload/."""
async def test_upload_file_success(self, auth_client: AsyncClient):
"""Test successful file upload."""
file_content = b"test file content"
files = {"file": ("testfile.txt", io.BytesIO(file_content), "text/plain")}
data = {"folder": "uploads"}
response = await auth_client.post("/api/files/upload/", files=files, data=data)
assert response.status_code == 201
data = response.json()
assert "file_path" in data
assert "file_url" in data
async def test_upload_file_unauthenticated(self, async_client: AsyncClient):
"""Test uploading file without auth returns 401."""
file_content = b"test file content"
files = {"file": ("testfile.txt", io.BytesIO(file_content), "text/plain")}
response = await async_client.post("/api/files/upload/", files=files)
assert response.status_code == 401
class TestGetFileInfoEndpoint:
"""Tests for GET /api/files/get_file/."""
async def test_get_file_info_success(self, auth_client: AsyncClient):
"""Test getting file info by path."""
response = await auth_client.get(
"/api/files/get_file/", params={"file_path": "uploads/test-file.txt"}
)
assert response.status_code == 200
data = response.json()
assert "file_path" in data
assert "file_url" in data
async def test_get_file_info_not_found(
self, auth_client: AsyncClient, mock_storage
):
"""Test getting info for nonexistent file returns 404."""
mock_storage.exists.return_value = False
response = await auth_client.get(
"/api/files/get_file/", params={"file_path": "nonexistent/file.txt"}
)
assert response.status_code == 404
async def test_get_file_info_unauthenticated(self, async_client: AsyncClient):
"""Test getting file info without auth returns 401."""
response = await async_client.get(
"/api/files/get_file/", params={"file_path": "uploads/test.txt"}
)
assert response.status_code == 401
class TestListFileEntriesEndpoint:
"""Tests for GET /api/files/files/."""
async def test_list_file_entries(self, auth_client: AsyncClient, test_file: File):
"""Test listing file entries."""
response = await auth_client.get("/api/files/files/")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
async def test_list_file_entries_unauthenticated(self, async_client: AsyncClient):
"""Test listing file entries without auth returns 401."""
response = await async_client.get("/api/files/files/")
assert response.status_code == 401
class TestCreateFileEntryEndpoint:
"""Tests for POST /api/files/files/."""
async def test_create_file_entry_success(self, auth_client: AsyncClient):
"""Test creating a file entry."""
response = await auth_client.post(
"/api/files/files/",
json={
"original_filename": "new-file.pdf",
"path": "uploads/new-file.pdf",
"storage_backend": "LOCAL",
"mime_type": "application/pdf",
"size_bytes": 4096,
},
)
assert response.status_code == 201
data = response.json()
assert data["original_filename"] == "new-file.pdf"
assert data["path"] == "uploads/new-file.pdf"
async def test_create_file_entry_unauthenticated(self, async_client: AsyncClient):
"""Test creating file entry without auth returns 401."""
response = await async_client.post(
"/api/files/files/",
json={
"original_filename": "test.pdf",
"path": "test.pdf",
"storage_backend": "LOCAL",
"mime_type": "application/pdf",
"size_bytes": 1024,
},
)
assert response.status_code == 401
class TestRetrieveFileEntryEndpoint:
"""Tests for GET /api/files/files/{file_id}/."""
async def test_retrieve_own_file_entry(
self, auth_client: AsyncClient, test_file: File
):
"""Test retrieving own file entry."""
response = await auth_client.get(f"/api/files/files/{test_file.id}/")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(test_file.id)
assert data["original_filename"] == test_file.original_filename
async def test_retrieve_other_file_as_staff(
self, staff_client: AsyncClient, test_file: File
):
"""Test staff can retrieve any file entry."""
response = await staff_client.get(f"/api/files/files/{test_file.id}/")
assert response.status_code == 200
async def test_retrieve_nonexistent_file_entry(self, auth_client: AsyncClient):
"""Test retrieving nonexistent file entry returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.get(f"/api/files/files/{fake_id}/")
assert response.status_code == 404
async def test_retrieve_other_file_forbidden(
self, auth_client: AsyncClient, other_file: File
):
"""Test regular user cannot retrieve other user's file entry."""
response = await auth_client.get(f"/api/files/files/{other_file.id}/")
assert response.status_code == 403
class TestPatchFileEntryEndpoint:
"""Tests for PATCH /api/files/files/{file_id}/."""
async def test_patch_own_file_entry(
self, auth_client: AsyncClient, test_file: File
):
"""Test updating own file entry."""
response = await auth_client.patch(
f"/api/files/files/{test_file.id}/",
json={"original_filename": "renamed-file.pdf"},
)
assert response.status_code == 200
data = response.json()
assert data["original_filename"] == "renamed-file.pdf"
async def test_patch_other_file_as_staff(
self, staff_client: AsyncClient, test_file: File
):
"""Test staff can update any file entry."""
response = await staff_client.patch(
f"/api/files/files/{test_file.id}/",
json={"original_filename": "staff-renamed.pdf"},
)
assert response.status_code == 200
async def test_patch_nonexistent_file_entry(self, auth_client: AsyncClient):
"""Test patching nonexistent file entry returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.patch(
f"/api/files/files/{fake_id}/",
json={"original_filename": "test.pdf"},
)
assert response.status_code == 404
async def test_patch_other_file_forbidden(
self, auth_client: AsyncClient, other_file: File
):
"""Test regular user cannot update other user's file entry."""
response = await auth_client.patch(
f"/api/files/files/{other_file.id}/",
json={"original_filename": "hacked.pdf"},
)
assert response.status_code == 403
class TestDeleteFileEntryEndpoint:
"""Tests for DELETE /api/files/files/{file_id}/."""
async def test_delete_own_file_entry(
self, auth_client: AsyncClient, test_file: File
):
"""Test deleting own file entry."""
response = await auth_client.delete(f"/api/files/files/{test_file.id}/")
assert response.status_code == 204
async def test_delete_other_file_as_staff(
self, staff_client: AsyncClient, test_file: File
):
"""Test staff can delete any file entry."""
response = await staff_client.delete(f"/api/files/files/{test_file.id}/")
assert response.status_code == 204
async def test_delete_nonexistent_file_entry(self, auth_client: AsyncClient):
"""Test deleting nonexistent file entry returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.delete(f"/api/files/files/{fake_id}/")
assert response.status_code == 404
async def test_delete_other_file_forbidden(
self, auth_client: AsyncClient, other_file: File
):
"""Test regular user cannot delete other user's file entry."""
response = await auth_client.delete(f"/api/files/files/{other_file.id}/")
assert response.status_code == 403
+365
View File
@@ -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
+461
View File
@@ -0,0 +1,461 @@
"""
Tests for media management endpoints.
"""
from __future__ import annotations
import uuid
from unittest.mock import AsyncMock, patch
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.modules.media.models import ArtifactMediaFile, MediaFile
from cpv3.modules.users.models import User
@pytest.fixture
async def test_media_file(test_db_session: AsyncSession, test_user: User) -> MediaFile:
"""Create a test media file owned by test_user."""
media_file = MediaFile(
id=uuid.uuid4(),
owner_id=test_user.id,
duration_seconds=120.5,
frame_rate=30.0,
width=1920,
height=1080,
is_deleted=False,
is_active=True,
)
test_db_session.add(media_file)
await test_db_session.commit()
await test_db_session.refresh(media_file)
return media_file
@pytest.fixture
async def other_media_file(
test_db_session: AsyncSession, other_user: User
) -> MediaFile:
"""Create a media file owned by another user."""
media_file = MediaFile(
id=uuid.uuid4(),
owner_id=other_user.id,
duration_seconds=60.0,
frame_rate=24.0,
width=1280,
height=720,
is_deleted=False,
is_active=True,
)
test_db_session.add(media_file)
await test_db_session.commit()
await test_db_session.refresh(media_file)
return media_file
@pytest.fixture
async def test_artifact(
test_db_session: AsyncSession, test_media_file: MediaFile
) -> ArtifactMediaFile:
"""Create a test artifact linked to test_media_file."""
artifact = ArtifactMediaFile(
id=uuid.uuid4(),
media_file_id=test_media_file.id,
artifact_type="THUMBNAIL",
is_deleted=False,
is_active=True,
)
test_db_session.add(artifact)
await test_db_session.commit()
await test_db_session.refresh(artifact)
return artifact
class TestGetMetaEndpoint:
"""Tests for GET /api/media/get_meta/."""
async def test_get_meta_success(self, auth_client: AsyncClient):
"""Test getting media metadata."""
with patch(
"cpv3.modules.media.router.probe_media",
new_callable=AsyncMock,
return_value={
"streams": [],
"format": {"filename": "test.mp4", "duration": "120.5"},
},
):
response = await auth_client.get(
"/api/media/get_meta/", params={"file_path": "uploads/test.mp4"}
)
assert response.status_code == 200
async def test_get_meta_unauthenticated(self, async_client: AsyncClient):
"""Test getting metadata without auth returns 401."""
response = await async_client.get(
"/api/media/get_meta/", params={"file_path": "test.mp4"}
)
assert response.status_code == 401
class TestSilenceRemoveEndpoint:
"""Tests for POST /api/media/silence_remove."""
async def test_silence_remove_success(self, auth_client: AsyncClient, mock_storage):
"""Test silence removal returns file info."""
with patch(
"cpv3.modules.media.router.remove_silence",
new_callable=AsyncMock,
) as mock_remove:
mock_remove.return_value = mock_storage.get_file_info.return_value
response = await auth_client.post(
"/api/media/silence_remove",
json={"file_path": "uploads/test.mp4", "folder": "processed"},
)
assert response.status_code == 200
data = response.json()
assert "file_path" in data
async def test_silence_remove_unauthenticated(self, async_client: AsyncClient):
"""Test silence removal without auth returns 401."""
response = await async_client.post(
"/api/media/silence_remove",
json={"file_path": "test.mp4"},
)
assert response.status_code == 401
class TestConvertEndpoint:
"""Tests for POST /api/media/convert."""
async def test_convert_success(self, auth_client: AsyncClient, mock_storage):
"""Test media conversion returns file info."""
with patch(
"cpv3.modules.media.router.convert_to_mp4",
new_callable=AsyncMock,
) as mock_convert:
mock_convert.return_value = mock_storage.get_file_info.return_value
response = await auth_client.post(
"/api/media/convert",
json={"file_path": "uploads/test.mov", "folder": "converted"},
)
assert response.status_code == 200
data = response.json()
assert "file_path" in data
async def test_convert_unauthenticated(self, async_client: AsyncClient):
"""Test conversion without auth returns 401."""
response = await async_client.post(
"/api/media/convert",
json={"file_path": "test.mov"},
)
assert response.status_code == 401
class TestListMediaFilesEndpoint:
"""Tests for GET /api/media/mediafiles/."""
async def test_list_mediafiles(
self, auth_client: AsyncClient, test_media_file: MediaFile
):
"""Test listing media files."""
response = await auth_client.get("/api/media/mediafiles/")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
async def test_list_mediafiles_unauthenticated(self, async_client: AsyncClient):
"""Test listing media files without auth returns 401."""
response = await async_client.get("/api/media/mediafiles/")
assert response.status_code == 401
class TestCreateMediaFileEndpoint:
"""Tests for POST /api/media/mediafiles/."""
async def test_create_mediafile_success(self, auth_client: AsyncClient):
"""Test creating a media file entry."""
response = await auth_client.post(
"/api/media/mediafiles/",
json={
"duration_seconds": 180.0,
"frame_rate": 60.0,
"width": 3840,
"height": 2160,
},
)
assert response.status_code == 201
data = response.json()
assert data["duration_seconds"] == 180.0
assert data["width"] == 3840
async def test_create_mediafile_unauthenticated(self, async_client: AsyncClient):
"""Test creating media file without auth returns 401."""
response = await async_client.post(
"/api/media/mediafiles/",
json={"duration_seconds": 60.0},
)
assert response.status_code == 401
class TestRetrieveMediaFileEndpoint:
"""Tests for GET /api/media/mediafiles/{media_file_id}/."""
async def test_retrieve_own_mediafile(
self, auth_client: AsyncClient, test_media_file: MediaFile
):
"""Test retrieving own media file."""
response = await auth_client.get(f"/api/media/mediafiles/{test_media_file.id}/")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(test_media_file.id)
async def test_retrieve_other_mediafile_as_staff(
self, staff_client: AsyncClient, test_media_file: MediaFile
):
"""Test staff can retrieve any media file."""
response = await staff_client.get(
f"/api/media/mediafiles/{test_media_file.id}/"
)
assert response.status_code == 200
async def test_retrieve_nonexistent_mediafile(self, auth_client: AsyncClient):
"""Test retrieving nonexistent media file returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.get(f"/api/media/mediafiles/{fake_id}/")
assert response.status_code == 404
async def test_retrieve_other_mediafile_forbidden(
self, auth_client: AsyncClient, other_media_file: MediaFile
):
"""Test regular user cannot retrieve other user's media file."""
response = await auth_client.get(
f"/api/media/mediafiles/{other_media_file.id}/"
)
assert response.status_code == 403
class TestPatchMediaFileEndpoint:
"""Tests for PATCH /api/media/mediafiles/{media_file_id}/."""
async def test_patch_own_mediafile(
self, auth_client: AsyncClient, test_media_file: MediaFile
):
"""Test updating own media file."""
response = await auth_client.patch(
f"/api/media/mediafiles/{test_media_file.id}/",
json={"notes": "Updated notes"},
)
assert response.status_code == 200
data = response.json()
assert data["notes"] == "Updated notes"
async def test_patch_other_mediafile_as_staff(
self, staff_client: AsyncClient, test_media_file: MediaFile
):
"""Test staff can update any media file."""
response = await staff_client.patch(
f"/api/media/mediafiles/{test_media_file.id}/",
json={"notes": "Staff updated"},
)
assert response.status_code == 200
async def test_patch_nonexistent_mediafile(self, auth_client: AsyncClient):
"""Test patching nonexistent media file returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.patch(
f"/api/media/mediafiles/{fake_id}/",
json={"notes": "test"},
)
assert response.status_code == 404
async def test_patch_other_mediafile_forbidden(
self, auth_client: AsyncClient, other_media_file: MediaFile
):
"""Test regular user cannot update other user's media file."""
response = await auth_client.patch(
f"/api/media/mediafiles/{other_media_file.id}/",
json={"notes": "hacked"},
)
assert response.status_code == 403
class TestDeleteMediaFileEndpoint:
"""Tests for DELETE /api/media/mediafiles/{media_file_id}/."""
async def test_delete_own_mediafile(
self, auth_client: AsyncClient, test_media_file: MediaFile
):
"""Test deleting own media file."""
response = await auth_client.delete(
f"/api/media/mediafiles/{test_media_file.id}/"
)
assert response.status_code == 204
async def test_delete_other_mediafile_as_staff(
self, staff_client: AsyncClient, test_media_file: MediaFile
):
"""Test staff can delete any media file."""
response = await staff_client.delete(
f"/api/media/mediafiles/{test_media_file.id}/"
)
assert response.status_code == 204
async def test_delete_nonexistent_mediafile(self, auth_client: AsyncClient):
"""Test deleting nonexistent media file returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.delete(f"/api/media/mediafiles/{fake_id}/")
assert response.status_code == 404
async def test_delete_other_mediafile_forbidden(
self, auth_client: AsyncClient, other_media_file: MediaFile
):
"""Test regular user cannot delete other user's media file."""
response = await auth_client.delete(
f"/api/media/mediafiles/{other_media_file.id}/"
)
assert response.status_code == 403
class TestListArtifactsEndpoint:
"""Tests for GET /api/media/artifacts/."""
async def test_list_artifacts(
self, auth_client: AsyncClient, test_artifact: ArtifactMediaFile
):
"""Test listing artifacts."""
response = await auth_client.get("/api/media/artifacts/")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
async def test_list_artifacts_unauthenticated(self, async_client: AsyncClient):
"""Test listing artifacts without auth returns 401."""
response = await async_client.get("/api/media/artifacts/")
assert response.status_code == 401
class TestCreateArtifactEndpoint:
"""Tests for POST /api/media/artifacts/."""
async def test_create_artifact_success(
self, auth_client: AsyncClient, test_media_file: MediaFile
):
"""Test creating an artifact."""
response = await auth_client.post(
"/api/media/artifacts/",
json={
"media_file_id": str(test_media_file.id),
"artifact_type": "AUDIO_PROXY",
},
)
assert response.status_code == 201
data = response.json()
assert data["artifact_type"] == "AUDIO_PROXY"
async def test_create_artifact_unauthenticated(self, async_client: AsyncClient):
"""Test creating artifact without auth returns 401."""
response = await async_client.post(
"/api/media/artifacts/",
json={
"media_file_id": str(uuid.uuid4()),
"artifact_type": "THUMBNAIL",
},
)
assert response.status_code == 401
class TestRetrieveArtifactEndpoint:
"""Tests for GET /api/media/artifacts/{artifact_id}/."""
async def test_retrieve_artifact(
self, auth_client: AsyncClient, test_artifact: ArtifactMediaFile
):
"""Test retrieving an artifact."""
response = await auth_client.get(f"/api/media/artifacts/{test_artifact.id}/")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(test_artifact.id)
async def test_retrieve_nonexistent_artifact(self, auth_client: AsyncClient):
"""Test retrieving nonexistent artifact returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.get(f"/api/media/artifacts/{fake_id}/")
assert response.status_code == 404
class TestPatchArtifactEndpoint:
"""Tests for PATCH /api/media/artifacts/{artifact_id}/."""
async def test_patch_artifact(
self, auth_client: AsyncClient, test_artifact: ArtifactMediaFile
):
"""Test updating an artifact."""
response = await auth_client.patch(
f"/api/media/artifacts/{test_artifact.id}/",
json={"is_deleted": True},
)
assert response.status_code == 200
data = response.json()
assert data["is_deleted"] is True
async def test_patch_nonexistent_artifact(self, auth_client: AsyncClient):
"""Test patching nonexistent artifact returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.patch(
f"/api/media/artifacts/{fake_id}/",
json={"is_deleted": True},
)
assert response.status_code == 404
class TestDeleteArtifactEndpoint:
"""Tests for DELETE /api/media/artifacts/{artifact_id}/."""
async def test_delete_artifact(
self, auth_client: AsyncClient, test_artifact: ArtifactMediaFile
):
"""Test deleting an artifact."""
response = await auth_client.delete(f"/api/media/artifacts/{test_artifact.id}/")
assert response.status_code == 204
async def test_delete_nonexistent_artifact(self, auth_client: AsyncClient):
"""Test deleting nonexistent artifact returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.delete(f"/api/media/artifacts/{fake_id}/")
assert response.status_code == 404
@@ -0,0 +1,254 @@
"""
Tests for project management endpoints.
"""
from __future__ import annotations
import uuid
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.modules.projects.models import Project
from cpv3.modules.users.models import User
@pytest.fixture
async def test_project(test_db_session: AsyncSession, test_user: User) -> Project:
"""Create a test project owned by test_user."""
project = Project(
id=uuid.uuid4(),
owner_id=test_user.id,
name="Test Project",
description="A test project",
language="en",
status="DRAFT",
is_active=True,
)
test_db_session.add(project)
await test_db_session.commit()
await test_db_session.refresh(project)
return project
@pytest.fixture
async def other_project(test_db_session: AsyncSession, other_user: User) -> Project:
"""Create a project owned by another user."""
project = Project(
id=uuid.uuid4(),
owner_id=other_user.id,
name="Other Project",
description="Another user's project",
language="en",
status="DRAFT",
is_active=True,
)
test_db_session.add(project)
await test_db_session.commit()
await test_db_session.refresh(project)
return project
class TestListProjectsEndpoint:
"""Tests for GET /api/projects/."""
async def test_list_projects_authenticated(
self, auth_client: AsyncClient, test_project: Project
):
"""Test listing projects as authenticated user."""
response = await auth_client.get("/api/projects/")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
async def test_list_projects_unauthenticated(self, async_client: AsyncClient):
"""Test listing projects without auth returns 401."""
response = await async_client.get("/api/projects/")
assert response.status_code == 401
class TestCreateProjectEndpoint:
"""Tests for POST /api/projects/."""
async def test_create_project_success(self, auth_client: AsyncClient):
"""Test creating a new project."""
response = await auth_client.post(
"/api/projects/",
json={
"name": "New Project",
"description": "A new project",
"language": "en",
},
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "New Project"
assert data["description"] == "A new project"
assert data["language"] == "en"
assert data["status"] == "DRAFT"
async def test_create_project_minimal(self, auth_client: AsyncClient):
"""Test creating a project with minimal fields."""
response = await auth_client.post(
"/api/projects/",
json={"name": "Minimal Project"},
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Minimal Project"
async def test_create_project_unauthenticated(self, async_client: AsyncClient):
"""Test creating project without auth returns 401."""
response = await async_client.post(
"/api/projects/",
json={"name": "Unauthorized Project"},
)
assert response.status_code == 401
class TestRetrieveProjectEndpoint:
"""Tests for GET /api/projects/{project_id}/."""
async def test_retrieve_own_project(
self, auth_client: AsyncClient, test_project: Project
):
"""Test retrieving own project."""
response = await auth_client.get(f"/api/projects/{test_project.id}/")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(test_project.id)
assert data["name"] == test_project.name
async def test_retrieve_other_project_as_staff(
self, staff_client: AsyncClient, test_project: Project
):
"""Test staff can retrieve any project."""
response = await staff_client.get(f"/api/projects/{test_project.id}/")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(test_project.id)
async def test_retrieve_nonexistent_project(self, auth_client: AsyncClient):
"""Test retrieving nonexistent project returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.get(f"/api/projects/{fake_id}/")
assert response.status_code == 404
assert response.json()["detail"] == "Not found"
async def test_retrieve_other_project_forbidden(
self, auth_client: AsyncClient, other_project: Project
):
"""Test regular user cannot retrieve other user's project."""
response = await auth_client.get(f"/api/projects/{other_project.id}/")
assert response.status_code == 403
assert response.json()["detail"] == "Forbidden"
class TestPatchProjectEndpoint:
"""Tests for PATCH /api/projects/{project_id}/."""
async def test_patch_own_project(
self, auth_client: AsyncClient, test_project: Project
):
"""Test updating own project."""
response = await auth_client.patch(
f"/api/projects/{test_project.id}/",
json={"name": "Updated Project", "description": "Updated description"},
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "Updated Project"
assert data["description"] == "Updated description"
async def test_patch_project_status(
self, auth_client: AsyncClient, test_project: Project
):
"""Test updating project status."""
response = await auth_client.patch(
f"/api/projects/{test_project.id}/",
json={"status": "PROCESSING"},
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "PROCESSING"
async def test_patch_other_project_as_staff(
self, staff_client: AsyncClient, test_project: Project
):
"""Test staff can update any project."""
response = await staff_client.patch(
f"/api/projects/{test_project.id}/",
json={"name": "Staff Updated"},
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "Staff Updated"
async def test_patch_nonexistent_project(self, auth_client: AsyncClient):
"""Test patching nonexistent project returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.patch(
f"/api/projects/{fake_id}/",
json={"name": "Test"},
)
assert response.status_code == 404
async def test_patch_other_project_forbidden(
self, auth_client: AsyncClient, other_project: Project
):
"""Test regular user cannot update other user's project."""
response = await auth_client.patch(
f"/api/projects/{other_project.id}/",
json={"name": "Hacked"},
)
assert response.status_code == 403
class TestDeleteProjectEndpoint:
"""Tests for DELETE /api/projects/{project_id}/."""
async def test_delete_own_project(
self, auth_client: AsyncClient, test_project: Project
):
"""Test deleting (deactivating) own project."""
response = await auth_client.delete(f"/api/projects/{test_project.id}/")
assert response.status_code == 204
async def test_delete_other_project_as_staff(
self, staff_client: AsyncClient, test_project: Project
):
"""Test staff can delete any project."""
response = await staff_client.delete(f"/api/projects/{test_project.id}/")
assert response.status_code == 204
async def test_delete_nonexistent_project(self, auth_client: AsyncClient):
"""Test deleting nonexistent project returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.delete(f"/api/projects/{fake_id}/")
assert response.status_code == 404
async def test_delete_other_project_forbidden(
self, auth_client: AsyncClient, other_project: Project
):
"""Test regular user cannot delete other user's project."""
response = await auth_client.delete(f"/api/projects/{other_project.id}/")
assert response.status_code == 403
@@ -0,0 +1,26 @@
"""
Tests for system endpoints (health check).
"""
from __future__ import annotations
import pytest
from httpx import AsyncClient
class TestSystemEndpoints:
"""Tests for GET /api/ping/."""
async def test_ping_returns_ok(self, async_client: AsyncClient):
"""Test health check endpoint returns ok status."""
response = await async_client.get("/api/ping/")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
async def test_ping_no_auth_required(self, async_client: AsyncClient):
"""Test health check endpoint works without authentication."""
# async_client has no auth header set
response = await async_client.get("/api/ping/")
assert response.status_code == 200
@@ -0,0 +1,295 @@
"""
Tests for transcription endpoints.
"""
from __future__ import annotations
import uuid
from unittest.mock import AsyncMock, patch
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.modules.files.models import File
from cpv3.modules.transcription.models import Transcription
from cpv3.modules.users.models import User
@pytest.fixture
async def source_file(test_db_session: AsyncSession, test_user: User) -> File:
"""Create a source file for transcription."""
file = File(
id=uuid.uuid4(),
owner_id=test_user.id,
original_filename="audio.mp3",
path="uploads/audio.mp3",
storage_backend="LOCAL",
mime_type="audio/mpeg",
size_bytes=5000000,
is_uploaded=True,
is_active=True,
)
test_db_session.add(file)
await test_db_session.commit()
await test_db_session.refresh(file)
return file
@pytest.fixture
async def test_transcription(
test_db_session: AsyncSession, source_file: File
) -> Transcription:
"""Create a test transcription."""
transcription = Transcription(
id=uuid.uuid4(),
source_file_id=source_file.id,
engine="LOCAL_WHISPER",
language="en",
document={"segments": []},
is_active=True,
)
test_db_session.add(transcription)
await test_db_session.commit()
await test_db_session.refresh(transcription)
return transcription
class TestListTranscriptionsEndpoint:
"""Tests for GET /api/transcribe/transcriptions/."""
async def test_list_transcriptions(
self, auth_client: AsyncClient, test_transcription: Transcription
):
"""Test listing transcriptions."""
response = await auth_client.get("/api/transcribe/transcriptions/")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
async def test_list_transcriptions_unauthenticated(self, async_client: AsyncClient):
"""Test listing transcriptions without auth returns 401."""
response = await async_client.get("/api/transcribe/transcriptions/")
assert response.status_code == 401
class TestCreateTranscriptionEndpoint:
"""Tests for POST /api/transcribe/transcriptions/."""
async def test_create_transcription_success(
self, auth_client: AsyncClient, source_file: File
):
"""Test creating a transcription entry."""
response = await auth_client.post(
"/api/transcribe/transcriptions/",
json={
"source_file_id": str(source_file.id),
"engine": "LOCAL_WHISPER",
"language": "en",
"document": {
"segments": [
{
"text": "Hello world",
"semantic_tags": [],
"structure_tags": [],
"time": {"start": 0.0, "end": 2.0},
"lines": [],
}
]
},
},
)
assert response.status_code == 201
data = response.json()
assert data["engine"] == "LOCAL_WHISPER"
assert data["language"] == "en"
async def test_create_transcription_unauthenticated(
self, async_client: AsyncClient
):
"""Test creating transcription without auth returns 401."""
response = await async_client.post(
"/api/transcribe/transcriptions/",
json={
"source_file_id": str(uuid.uuid4()),
"document": {"segments": []},
},
)
assert response.status_code == 401
class TestRetrieveTranscriptionEndpoint:
"""Tests for GET /api/transcribe/transcriptions/{transcription_id}/."""
async def test_retrieve_transcription(
self, auth_client: AsyncClient, test_transcription: Transcription
):
"""Test retrieving a transcription."""
response = await auth_client.get(
f"/api/transcribe/transcriptions/{test_transcription.id}/"
)
assert response.status_code == 200
data = response.json()
assert data["id"] == str(test_transcription.id)
assert data["engine"] == test_transcription.engine
async def test_retrieve_nonexistent_transcription(self, auth_client: AsyncClient):
"""Test retrieving nonexistent transcription returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.get(f"/api/transcribe/transcriptions/{fake_id}/")
assert response.status_code == 404
class TestPatchTranscriptionEndpoint:
"""Tests for PATCH /api/transcribe/transcriptions/{transcription_id}/."""
async def test_patch_transcription(
self, auth_client: AsyncClient, test_transcription: Transcription
):
"""Test updating a transcription."""
updated_document = {
"segments": [
{
"text": "Updated text",
"semantic_tags": [],
"structure_tags": [],
"time": {"start": 0.0, "end": 3.0},
"lines": [],
}
]
}
response = await auth_client.patch(
f"/api/transcribe/transcriptions/{test_transcription.id}/",
json={"document": updated_document},
)
assert response.status_code == 200
data = response.json()
assert data["document"]["segments"][0]["text"] == "Updated text"
async def test_patch_nonexistent_transcription(self, auth_client: AsyncClient):
"""Test patching nonexistent transcription returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.patch(
f"/api/transcribe/transcriptions/{fake_id}/",
json={"document": {"segments": []}},
)
assert response.status_code == 404
class TestDeleteTranscriptionEndpoint:
"""Tests for DELETE /api/transcribe/transcriptions/{transcription_id}/."""
async def test_delete_transcription(
self, auth_client: AsyncClient, test_transcription: Transcription
):
"""Test deleting a transcription."""
response = await auth_client.delete(
f"/api/transcribe/transcriptions/{test_transcription.id}/"
)
assert response.status_code == 204
async def test_delete_nonexistent_transcription(self, auth_client: AsyncClient):
"""Test deleting nonexistent transcription returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.delete(
f"/api/transcribe/transcriptions/{fake_id}/"
)
assert response.status_code == 404
class TestWhisperTranscribeEndpoint:
"""Tests for POST /api/transcribe/whisper/."""
async def test_whisper_transcribe_success(self, auth_client: AsyncClient):
"""Test Whisper transcription endpoint."""
mock_result = {
"segments": [
{
"text": "Hello from Whisper",
"semantic_tags": [],
"structure_tags": [],
"time": {"start": 0.0, "end": 2.5},
"lines": [],
}
]
}
with patch(
"cpv3.modules.transcription.router.transcribe_with_whisper",
new_callable=AsyncMock,
return_value=mock_result,
):
response = await auth_client.post(
"/api/transcribe/whisper/",
json={
"file_path": "uploads/audio.mp3",
"model_name": "tiny",
"language": "en",
},
)
assert response.status_code == 200
async def test_whisper_transcribe_unauthenticated(self, async_client: AsyncClient):
"""Test Whisper transcription without auth returns 401."""
response = await async_client.post(
"/api/transcribe/whisper/",
json={"file_path": "test.mp3"},
)
assert response.status_code == 401
class TestGoogleSpeechTranscribeEndpoint:
"""Tests for POST /api/transcribe/google-speech/."""
async def test_google_speech_transcribe_success(self, auth_client: AsyncClient):
"""Test Google Speech transcription endpoint."""
mock_result = {
"segments": [
{
"text": "Hello from Google",
"semantic_tags": [],
"structure_tags": [],
"time": {"start": 0.0, "end": 2.0},
"lines": [],
}
]
}
with patch(
"cpv3.modules.transcription.router.transcribe_with_google_speech",
new_callable=AsyncMock,
return_value=mock_result,
):
response = await auth_client.post(
"/api/transcribe/google-speech/",
json={
"file_path": "uploads/audio.mp3",
"language_codes": ["en-US"],
},
)
assert response.status_code == 200
async def test_google_speech_transcribe_unauthenticated(
self, async_client: AsyncClient
):
"""Test Google Speech transcription without auth returns 401."""
response = await async_client.post(
"/api/transcribe/google-speech/",
json={"file_path": "test.mp3"},
)
assert response.status_code == 401
+212
View File
@@ -0,0 +1,212 @@
"""
Tests for user management endpoints.
"""
from __future__ import annotations
import uuid
import pytest
from httpx import AsyncClient
from cpv3.modules.users.models import User
class TestListUsersEndpoint:
"""Tests for GET /api/users/."""
async def test_list_users_authenticated(
self, auth_client: AsyncClient, test_user: User
):
"""Test listing users as authenticated user."""
response = await auth_client.get("/api/users/")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
async def test_list_users_unauthenticated(self, async_client: AsyncClient):
"""Test listing users without auth returns 401."""
response = await async_client.get("/api/users/")
assert response.status_code == 401
class TestCreateUserEndpoint:
"""Tests for POST /api/users/."""
async def test_create_user_as_staff(self, staff_client: AsyncClient):
"""Test staff can create a new user."""
response = await staff_client.post(
"/api/users/",
json={
"username": "newcreateduser",
"email": "newcreated@example.com",
"password": "password123",
"first_name": "New",
"last_name": "Created",
},
)
assert response.status_code == 201
data = response.json()
assert data["username"] == "newcreateduser"
assert data["email"] == "newcreated@example.com"
async def test_create_user_unauthenticated(self, async_client: AsyncClient):
"""Test creating user without auth returns 401."""
response = await async_client.post(
"/api/users/",
json={
"username": "newuser",
"email": "new@example.com",
"password": "password123",
},
)
assert response.status_code == 401
class TestMeEndpoint:
"""Tests for GET /api/users/me/."""
async def test_me_returns_current_user(
self, auth_client: AsyncClient, test_user: User
):
"""Test getting current user info."""
response = await auth_client.get("/api/users/me/")
assert response.status_code == 200
data = response.json()
assert data["username"] == test_user.username
assert data["email"] == test_user.email
assert data["id"] == str(test_user.id)
async def test_me_unauthenticated(self, async_client: AsyncClient):
"""Test getting current user without auth returns 401."""
response = await async_client.get("/api/users/me/")
assert response.status_code == 401
class TestRetrieveUserEndpoint:
"""Tests for GET /api/users/{user_id}/."""
async def test_retrieve_own_user(self, auth_client: AsyncClient, test_user: User):
"""Test user can retrieve their own info."""
response = await auth_client.get(f"/api/users/{test_user.id}/")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(test_user.id)
assert data["username"] == test_user.username
async def test_retrieve_other_user_as_staff(
self, staff_client: AsyncClient, test_user: User
):
"""Test staff can retrieve other user's info."""
response = await staff_client.get(f"/api/users/{test_user.id}/")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(test_user.id)
async def test_retrieve_nonexistent_user(self, auth_client: AsyncClient):
"""Test retrieving nonexistent user returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.get(f"/api/users/{fake_id}/")
assert response.status_code == 404
assert response.json()["detail"] == "Not found"
async def test_retrieve_other_user_forbidden(
self, auth_client: AsyncClient, other_user: User
):
"""Test regular user cannot retrieve other user's info."""
response = await auth_client.get(f"/api/users/{other_user.id}/")
assert response.status_code == 403
assert response.json()["detail"] == "Forbidden"
class TestPatchUserEndpoint:
"""Tests for PATCH /api/users/{user_id}/."""
async def test_patch_own_user(self, auth_client: AsyncClient, test_user: User):
"""Test user can update their own info."""
response = await auth_client.patch(
f"/api/users/{test_user.id}/",
json={"first_name": "Updated", "last_name": "Name"},
)
assert response.status_code == 200
data = response.json()
assert data["first_name"] == "Updated"
assert data["last_name"] == "Name"
async def test_patch_other_user_as_staff(
self, staff_client: AsyncClient, test_user: User
):
"""Test staff can update other user's info."""
response = await staff_client.patch(
f"/api/users/{test_user.id}/",
json={"first_name": "StaffUpdated"},
)
assert response.status_code == 200
data = response.json()
assert data["first_name"] == "StaffUpdated"
async def test_patch_nonexistent_user(self, auth_client: AsyncClient):
"""Test patching nonexistent user returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.patch(
f"/api/users/{fake_id}/",
json={"first_name": "Test"},
)
assert response.status_code == 404
async def test_patch_other_user_forbidden(
self, auth_client: AsyncClient, other_user: User
):
"""Test regular user cannot update other user's info."""
response = await auth_client.patch(
f"/api/users/{other_user.id}/",
json={"first_name": "Hacked"},
)
assert response.status_code == 403
class TestDeleteUserEndpoint:
"""Tests for DELETE /api/users/{user_id}/."""
async def test_delete_own_user(self, auth_client: AsyncClient, test_user: User):
"""Test user can delete (deactivate) their own account."""
response = await auth_client.delete(f"/api/users/{test_user.id}/")
assert response.status_code == 204
async def test_delete_other_user_as_staff(
self, staff_client: AsyncClient, test_user: User
):
"""Test staff can delete other user's account."""
response = await staff_client.delete(f"/api/users/{test_user.id}/")
assert response.status_code == 204
async def test_delete_nonexistent_user(self, auth_client: AsyncClient):
"""Test deleting nonexistent user returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.delete(f"/api/users/{fake_id}/")
assert response.status_code == 404
async def test_delete_other_user_forbidden(
self, auth_client: AsyncClient, other_user: User
):
"""Test regular user cannot delete other user's account."""
response = await auth_client.delete(f"/api/users/{other_user.id}/")
assert response.status_code == 403
@@ -0,0 +1,247 @@
"""
Tests for webhooks endpoints.
"""
from __future__ import annotations
import uuid
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.modules.webhooks.models import Webhook
from cpv3.modules.users.models import User
@pytest.fixture
async def test_webhook(test_db_session: AsyncSession, test_user: User) -> Webhook:
"""Create a test webhook owned by test_user."""
webhook = Webhook(
id=uuid.uuid4(),
user_id=test_user.id,
event="job.completed",
url="https://example.com/webhook",
secret="test-secret-123",
is_active=True,
)
test_db_session.add(webhook)
await test_db_session.commit()
await test_db_session.refresh(webhook)
return webhook
@pytest.fixture
async def other_webhook(test_db_session: AsyncSession, other_user: User) -> Webhook:
"""Create a webhook owned by another user."""
webhook = Webhook(
id=uuid.uuid4(),
user_id=other_user.id,
event="job.failed",
url="https://other.com/webhook",
secret="other-secret-456",
is_active=True,
)
test_db_session.add(webhook)
await test_db_session.commit()
await test_db_session.refresh(webhook)
return webhook
class TestListWebhooksEndpoint:
"""Tests for GET /api/webhooks/."""
async def test_list_webhooks(self, auth_client: AsyncClient, test_webhook: Webhook):
"""Test listing webhooks."""
response = await auth_client.get("/api/webhooks/")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
async def test_list_webhooks_unauthenticated(self, async_client: AsyncClient):
"""Test listing webhooks without auth returns 401."""
response = await async_client.get("/api/webhooks/")
assert response.status_code == 401
class TestCreateWebhookEndpoint:
"""Tests for POST /api/webhooks/."""
async def test_create_webhook_success(self, auth_client: AsyncClient):
"""Test creating a webhook."""
response = await auth_client.post(
"/api/webhooks/",
json={
"event": "transcription.completed",
"url": "https://myapp.com/webhook",
"secret": "my-webhook-secret",
},
)
assert response.status_code == 201
data = response.json()
assert data["event"] == "transcription.completed"
assert data["url"] == "https://myapp.com/webhook"
async def test_create_webhook_minimal(self, auth_client: AsyncClient):
"""Test creating a webhook with minimal fields."""
response = await auth_client.post(
"/api/webhooks/",
json={"url": "https://minimal.com/hook"},
)
assert response.status_code == 201
data = response.json()
assert data["url"] == "https://minimal.com/hook"
async def test_create_webhook_unauthenticated(self, async_client: AsyncClient):
"""Test creating webhook without auth returns 401."""
response = await async_client.post(
"/api/webhooks/",
json={"url": "https://test.com/hook"},
)
assert response.status_code == 401
class TestRetrieveWebhookEndpoint:
"""Tests for GET /api/webhooks/{webhook_id}/."""
async def test_retrieve_own_webhook(
self, auth_client: AsyncClient, test_webhook: Webhook
):
"""Test retrieving own webhook."""
response = await auth_client.get(f"/api/webhooks/{test_webhook.id}/")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(test_webhook.id)
assert data["url"] == test_webhook.url
async def test_retrieve_other_webhook_as_staff(
self, staff_client: AsyncClient, test_webhook: Webhook
):
"""Test staff can retrieve any webhook."""
response = await staff_client.get(f"/api/webhooks/{test_webhook.id}/")
assert response.status_code == 200
async def test_retrieve_nonexistent_webhook(self, auth_client: AsyncClient):
"""Test retrieving nonexistent webhook returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.get(f"/api/webhooks/{fake_id}/")
assert response.status_code == 404
assert response.json()["detail"] == "Not found"
async def test_retrieve_other_webhook_forbidden(
self, auth_client: AsyncClient, other_webhook: Webhook
):
"""Test regular user cannot retrieve other user's webhook."""
response = await auth_client.get(f"/api/webhooks/{other_webhook.id}/")
assert response.status_code == 403
assert response.json()["detail"] == "Forbidden"
class TestPatchWebhookEndpoint:
"""Tests for PATCH /api/webhooks/{webhook_id}/."""
async def test_patch_own_webhook(
self, auth_client: AsyncClient, test_webhook: Webhook
):
"""Test updating own webhook."""
response = await auth_client.patch(
f"/api/webhooks/{test_webhook.id}/",
json={
"url": "https://updated.com/webhook",
"event": "job.started",
},
)
assert response.status_code == 200
data = response.json()
assert data["url"] == "https://updated.com/webhook"
assert data["event"] == "job.started"
async def test_patch_webhook_deactivate(
self, auth_client: AsyncClient, test_webhook: Webhook
):
"""Test deactivating a webhook."""
response = await auth_client.patch(
f"/api/webhooks/{test_webhook.id}/",
json={"is_active": False},
)
assert response.status_code == 200
data = response.json()
assert data["is_active"] is False
async def test_patch_other_webhook_as_staff(
self, staff_client: AsyncClient, test_webhook: Webhook
):
"""Test staff can update any webhook."""
response = await staff_client.patch(
f"/api/webhooks/{test_webhook.id}/",
json={"event": "staff.updated"},
)
assert response.status_code == 200
async def test_patch_nonexistent_webhook(self, auth_client: AsyncClient):
"""Test patching nonexistent webhook returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.patch(
f"/api/webhooks/{fake_id}/",
json={"url": "https://test.com"},
)
assert response.status_code == 404
async def test_patch_other_webhook_forbidden(
self, auth_client: AsyncClient, other_webhook: Webhook
):
"""Test regular user cannot update other user's webhook."""
response = await auth_client.patch(
f"/api/webhooks/{other_webhook.id}/",
json={"url": "https://hacked.com"},
)
assert response.status_code == 403
class TestDeleteWebhookEndpoint:
"""Tests for DELETE /api/webhooks/{webhook_id}/."""
async def test_delete_own_webhook(
self, auth_client: AsyncClient, test_webhook: Webhook
):
"""Test deleting own webhook."""
response = await auth_client.delete(f"/api/webhooks/{test_webhook.id}/")
assert response.status_code == 204
async def test_delete_other_webhook_as_staff(
self, staff_client: AsyncClient, test_webhook: Webhook
):
"""Test staff can delete any webhook."""
response = await staff_client.delete(f"/api/webhooks/{test_webhook.id}/")
assert response.status_code == 204
async def test_delete_nonexistent_webhook(self, auth_client: AsyncClient):
"""Test deleting nonexistent webhook returns 404."""
fake_id = uuid.uuid4()
response = await auth_client.delete(f"/api/webhooks/{fake_id}/")
assert response.status_code == 404
async def test_delete_other_webhook_forbidden(
self, auth_client: AsyncClient, other_webhook: Webhook
):
"""Test regular user cannot delete other user's webhook."""
response = await auth_client.delete(f"/api/webhooks/{other_webhook.id}/")
assert response.status_code == 403