feat: download files, show cloud video, solf deletion with no reprocessing (#920)

* fix: move upd ports out of MacOS internal Range

* feat: download files, show cloud video, solf deletion with no reprocessing
This commit is contained in:
Juan Diego García
2026-03-20 11:04:53 -05:00
committed by GitHub
parent cb1beae90d
commit a76f114378
21 changed files with 1413 additions and 77 deletions

View File

@@ -76,8 +76,10 @@ async def test_cleanup_old_public_data_deletes_old_anonymous_transcripts():
assert result["transcripts_deleted"] == 1
assert result["errors"] == []
# Verify old anonymous transcript was deleted
assert await transcripts_controller.get_by_id(old_transcript.id) is None
# Verify old anonymous transcript was soft-deleted
old = await transcripts_controller.get_by_id(old_transcript.id)
assert old is not None
assert old.deleted_at is not None
# Verify new anonymous transcript still exists
assert await transcripts_controller.get_by_id(new_transcript.id) is not None
@@ -150,15 +152,17 @@ async def test_cleanup_deletes_associated_meeting_and_recording():
assert result["recordings_deleted"] == 1
assert result["errors"] == []
# Verify transcript was deleted
assert await transcripts_controller.get_by_id(old_transcript.id) is None
# Verify transcript was soft-deleted
old = await transcripts_controller.get_by_id(old_transcript.id)
assert old is not None
assert old.deleted_at is not None
# Verify meeting was deleted
# Verify meeting was hard-deleted (cleanup deletes meetings directly)
query = meetings.select().where(meetings.c.id == meeting_id)
meeting_result = await get_database().fetch_one(query)
assert meeting_result is None
# Verify recording was deleted
# Verify recording was hard-deleted (cleanup deletes recordings directly)
assert await recordings_controller.get_by_id(recording.id) is None

View File

@@ -1,7 +1,8 @@
import pytest
from reflector.db.recordings import Recording, recordings_controller
from reflector.db.rooms import rooms_controller
from reflector.db.transcripts import transcripts_controller
from reflector.db.transcripts import SourceKind, transcripts_controller
@pytest.mark.asyncio
@@ -192,9 +193,93 @@ async def test_transcript_delete(authenticated_client, client):
assert response.status_code == 200
assert response.json()["status"] == "ok"
# API returns 404 for soft-deleted transcripts
response = await client.get(f"/transcripts/{tid}")
assert response.status_code == 404
# But the transcript still exists in DB with deleted_at set
transcript = await transcripts_controller.get_by_id(tid)
assert transcript is not None
assert transcript.deleted_at is not None
@pytest.mark.asyncio
async def test_deleted_transcript_not_in_list(authenticated_client, client):
"""Soft-deleted transcripts should not appear in the list endpoint."""
response = await client.post("/transcripts", json={"name": "testdel_list"})
assert response.status_code == 200
tid = response.json()["id"]
# Verify it appears in the list
response = await client.get("/transcripts")
assert response.status_code == 200
ids = [t["id"] for t in response.json()["items"]]
assert tid in ids
# Delete it
response = await client.delete(f"/transcripts/{tid}")
assert response.status_code == 200
# Verify it no longer appears in the list
response = await client.get("/transcripts")
assert response.status_code == 200
ids = [t["id"] for t in response.json()["items"]]
assert tid not in ids
@pytest.mark.asyncio
async def test_delete_already_deleted_is_idempotent(authenticated_client, client):
"""Deleting an already-deleted transcript is idempotent (returns 200)."""
response = await client.post("/transcripts", json={"name": "testdel_idem"})
assert response.status_code == 200
tid = response.json()["id"]
# First delete
response = await client.delete(f"/transcripts/{tid}")
assert response.status_code == 200
# Second delete — idempotent, still returns ok
response = await client.delete(f"/transcripts/{tid}")
assert response.status_code == 200
# But deleted_at was only set once (not updated)
transcript = await transcripts_controller.get_by_id(tid)
assert transcript is not None
assert transcript.deleted_at is not None
@pytest.mark.asyncio
async def test_deleted_transcript_recording_soft_deleted(authenticated_client, client):
"""Soft-deleting a transcript also soft-deletes its recording."""
from datetime import datetime, timezone
recording = await recordings_controller.create(
Recording(
bucket_name="test-bucket",
object_key="test.mp4",
recorded_at=datetime.now(timezone.utc),
)
)
transcript = await transcripts_controller.add(
name="with-recording",
source_kind=SourceKind.ROOM,
recording_id=recording.id,
user_id="randomuserid",
)
response = await client.delete(f"/transcripts/{transcript.id}")
assert response.status_code == 200
# Recording still in DB with deleted_at set
rec = await recordings_controller.get_by_id(recording.id)
assert rec is not None
assert rec.deleted_at is not None
# Transcript still in DB with deleted_at set
tr = await transcripts_controller.get_by_id(transcript.id)
assert tr is not None
assert tr.deleted_at is not None
@pytest.mark.asyncio
async def test_transcript_mark_reviewed(authenticated_client, client):

View File

@@ -0,0 +1,36 @@
import io
import zipfile
import pytest
@pytest.mark.asyncio
async def test_download_zip_returns_valid_zip(
authenticated_client, client, fake_transcript_with_topics
):
"""Test that the zip download endpoint returns a valid zip file."""
transcript = fake_transcript_with_topics
response = await client.get(f"/transcripts/{transcript.id}/download/zip")
assert response.status_code == 200
assert response.headers["content-type"] == "application/zip"
# Verify it's a valid zip
zip_buffer = io.BytesIO(response.content)
with zipfile.ZipFile(zip_buffer) as zf:
names = zf.namelist()
assert "metadata.json" in names
assert "audio.mp3" in names
@pytest.mark.asyncio
async def test_download_zip_requires_auth(client):
"""Test that zip download requires authentication."""
response = await client.get("/transcripts/nonexistent/download/zip")
assert response.status_code in (401, 403, 422)
@pytest.mark.asyncio
async def test_download_zip_not_found(authenticated_client, client):
"""Test 404 for non-existent transcript."""
response = await client.get("/transcripts/nonexistent-id/download/zip")
assert response.status_code == 404

View File

@@ -1,5 +1,4 @@
from datetime import datetime, timezone
from unittest.mock import AsyncMock, patch
import pytest
@@ -9,6 +8,7 @@ from reflector.db.transcripts import SourceKind, transcripts_controller
@pytest.mark.asyncio
async def test_recording_deleted_with_transcript():
"""Soft-delete: recording and transcript remain in DB with deleted_at set, no files deleted."""
recording = await recordings_controller.create(
Recording(
bucket_name="test-bucket",
@@ -22,16 +22,13 @@ async def test_recording_deleted_with_transcript():
recording_id=recording.id,
)
with patch("reflector.db.transcripts.get_transcripts_storage") as mock_get_storage:
storage_instance = mock_get_storage.return_value
storage_instance.delete_file = AsyncMock()
await transcripts_controller.remove_by_id(transcript.id)
await transcripts_controller.remove_by_id(transcript.id)
# Both should still exist in DB but with deleted_at set
rec = await recordings_controller.get_by_id(recording.id)
assert rec is not None
assert rec.deleted_at is not None
# Should be called with bucket override
storage_instance.delete_file.assert_awaited_once_with(
recording.object_key, bucket=recording.bucket_name
)
assert await recordings_controller.get_by_id(recording.id) is None
assert await transcripts_controller.get_by_id(transcript.id) is None
tr = await transcripts_controller.get_by_id(transcript.id)
assert tr is not None
assert tr.deleted_at is not None

View File

@@ -0,0 +1,105 @@
from datetime import datetime, timedelta, timezone
from unittest.mock import AsyncMock, patch
import pytest
from reflector.db.transcripts import SourceKind, transcripts_controller
@pytest.mark.asyncio
async def test_video_url_returns_404_when_no_meeting(authenticated_client, client):
"""Test that video URL returns 404 when transcript has no meeting."""
response = await client.post("/transcripts", json={"name": "no-meeting"})
assert response.status_code == 200
tid = response.json()["id"]
response = await client.get(f"/transcripts/{tid}/video/url")
assert response.status_code == 404
@pytest.mark.asyncio
async def test_video_url_returns_404_when_no_cloud_video(authenticated_client, client):
"""Test that video URL returns 404 when meeting has no cloud video."""
from reflector.db import get_database
from reflector.db.meetings import meetings
meeting_id = "test-meeting-no-video"
await get_database().execute(
meetings.insert().values(
id=meeting_id,
room_name="No Video Meeting",
room_url="https://example.com",
host_room_url="https://example.com/host",
start_date=datetime.now(timezone.utc),
end_date=datetime.now(timezone.utc) + timedelta(hours=1),
room_id=None,
)
)
transcript = await transcripts_controller.add(
name="with-meeting",
source_kind=SourceKind.ROOM,
meeting_id=meeting_id,
user_id="randomuserid",
)
response = await client.get(f"/transcripts/{transcript.id}/video/url")
assert response.status_code == 404
@pytest.mark.asyncio
async def test_video_url_returns_presigned_url(authenticated_client, client):
"""Test that video URL returns a presigned URL when cloud video exists."""
from reflector.db import get_database
from reflector.db.meetings import meetings
meeting_id = "test-meeting-with-video"
await get_database().execute(
meetings.insert().values(
id=meeting_id,
room_name="Video Meeting",
room_url="https://example.com",
host_room_url="https://example.com/host",
start_date=datetime.now(timezone.utc),
end_date=datetime.now(timezone.utc) + timedelta(hours=1),
room_id=None,
daily_composed_video_s3_key="recordings/video.mp4",
daily_composed_video_duration=120,
)
)
transcript = await transcripts_controller.add(
name="with-video",
source_kind=SourceKind.ROOM,
meeting_id=meeting_id,
user_id="randomuserid",
)
with patch("reflector.views.transcripts_video.get_source_storage") as mock_storage:
mock_instance = AsyncMock()
mock_instance.get_file_url = AsyncMock(
return_value="https://s3.example.com/presigned-url"
)
mock_storage.return_value = mock_instance
response = await client.get(f"/transcripts/{transcript.id}/video/url")
assert response.status_code == 200
data = response.json()
assert data["url"] == "https://s3.example.com/presigned-url"
assert data["duration"] == 120
assert data["content_type"] == "video/mp4"
@pytest.mark.asyncio
async def test_transcript_get_includes_video_fields(authenticated_client, client):
"""Test that transcript GET response includes has_cloud_video field."""
response = await client.post("/transcripts", json={"name": "video-fields"})
assert response.status_code == 200
tid = response.json()["id"]
response = await client.get(f"/transcripts/{tid}")
assert response.status_code == 200
data = response.json()
assert data["has_cloud_video"] is False
assert data["cloud_video_duration"] is None