mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-04-14 17:26:55 +00:00
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:
committed by
GitHub
parent
cb1beae90d
commit
a76f114378
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
36
server/tests/test_transcripts_download.py
Normal file
36
server/tests/test_transcripts_download.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
105
server/tests/test_transcripts_video.py
Normal file
105
server/tests/test_transcripts_video.py
Normal 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
|
||||
Reference in New Issue
Block a user