mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-04-01 03:46:47 +00:00
* feat: show trash for soft deleted transcripts and hard delete option * fix: test fixtures * docs: aws new permissions
857 lines
29 KiB
Python
857 lines
29 KiB
Python
from datetime import datetime, timedelta, timezone
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from reflector.db.meetings import meetings_controller
|
|
from reflector.db.recordings import Recording, recordings_controller
|
|
from reflector.db.rooms import rooms_controller
|
|
from reflector.db.transcripts import SourceKind, transcripts_controller
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_create(monkeypatch, client):
|
|
from reflector.settings import settings
|
|
|
|
monkeypatch.setattr(
|
|
settings, "PUBLIC_MODE", True
|
|
) # public mode: allow anonymous transcript creation for this test
|
|
response = await client.post("/transcripts", json={"name": "test"})
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "test"
|
|
assert response.json()["status"] == "idle"
|
|
assert response.json()["locked"] is False
|
|
assert response.json()["id"] is not None
|
|
assert response.json()["created_at"] is not None
|
|
|
|
# ensure some fields are not returned
|
|
assert "topics" not in response.json()
|
|
assert "events" not in response.json()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_get_update_name(authenticated_client, client):
|
|
response = await client.post("/transcripts", json={"name": "test"})
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "test"
|
|
|
|
tid = response.json()["id"]
|
|
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "test"
|
|
|
|
response = await client.patch(f"/transcripts/{tid}", json={"name": "test2"})
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "test2"
|
|
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "test2"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_get_update_locked(authenticated_client, client):
|
|
response = await client.post("/transcripts", json={"name": "test"})
|
|
assert response.status_code == 200
|
|
assert response.json()["locked"] is False
|
|
|
|
tid = response.json()["id"]
|
|
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["locked"] is False
|
|
|
|
response = await client.patch(f"/transcripts/{tid}", json={"locked": True})
|
|
assert response.status_code == 200
|
|
assert response.json()["locked"] is True
|
|
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["locked"] is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_get_update_summary(authenticated_client, client):
|
|
response = await client.post("/transcripts", json={"name": "test"})
|
|
assert response.status_code == 200
|
|
assert response.json()["long_summary"] is None
|
|
assert response.json()["short_summary"] is None
|
|
|
|
tid = response.json()["id"]
|
|
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["long_summary"] is None
|
|
assert response.json()["short_summary"] is None
|
|
|
|
response = await client.patch(
|
|
f"/transcripts/{tid}",
|
|
json={"long_summary": "test_long", "short_summary": "test_short"},
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["long_summary"] == "test_long"
|
|
assert response.json()["short_summary"] == "test_short"
|
|
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["long_summary"] == "test_long"
|
|
assert response.json()["short_summary"] == "test_short"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_get_update_title(authenticated_client, client):
|
|
response = await client.post("/transcripts", json={"name": "test"})
|
|
assert response.status_code == 200
|
|
assert response.json()["title"] is None
|
|
|
|
tid = response.json()["id"]
|
|
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["title"] is None
|
|
|
|
response = await client.patch(f"/transcripts/{tid}", json={"title": "test_title"})
|
|
assert response.status_code == 200
|
|
assert response.json()["title"] == "test_title"
|
|
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["title"] == "test_title"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_status_emits_status_event_and_updates_transcript(
|
|
monkeypatch, client
|
|
):
|
|
"""set_status adds a STATUS event and updates the transcript status (broadcast for WebSocket)."""
|
|
from reflector.settings import settings
|
|
|
|
monkeypatch.setattr(
|
|
settings, "PUBLIC_MODE", True
|
|
) # public mode: allow anonymous transcript creation for this test
|
|
response = await client.post("/transcripts", json={"name": "Status test"})
|
|
assert response.status_code == 200
|
|
transcript_id = response.json()["id"]
|
|
|
|
transcript = await transcripts_controller.get_by_id(transcript_id)
|
|
assert transcript is not None
|
|
assert transcript.status == "idle"
|
|
|
|
event = await transcripts_controller.set_status(transcript_id, "processing")
|
|
assert event is not None
|
|
assert event.event == "STATUS"
|
|
assert event.data.get("value") == "processing"
|
|
|
|
updated = await transcripts_controller.get_by_id(transcript_id)
|
|
assert updated.status == "processing"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcripts_list_anonymous(client):
|
|
# XXX this test is a bit fragile, as it depends on the storage which
|
|
# is shared between tests
|
|
from reflector.settings import settings
|
|
|
|
response = await client.get("/transcripts")
|
|
assert response.status_code == 401
|
|
|
|
# if public mode, it should be allowed
|
|
try:
|
|
settings.PUBLIC_MODE = True
|
|
response = await client.get("/transcripts")
|
|
assert response.status_code == 200
|
|
finally:
|
|
settings.PUBLIC_MODE = False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcripts_list_authenticated(authenticated_client, client):
|
|
# XXX this test is a bit fragile, as it depends on the storage which
|
|
# is shared between tests
|
|
|
|
response = await client.post("/transcripts", json={"name": "testxx1"})
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "testxx1"
|
|
|
|
response = await client.post("/transcripts", json={"name": "testxx2"})
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "testxx2"
|
|
|
|
response = await client.get("/transcripts")
|
|
assert response.status_code == 200
|
|
assert len(response.json()["items"]) >= 2
|
|
names = [t["name"] for t in response.json()["items"]]
|
|
assert "testxx1" in names
|
|
assert "testxx2" in names
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_delete(authenticated_client, client):
|
|
response = await client.post("/transcripts", json={"name": "testdel1"})
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "testdel1"
|
|
|
|
tid = response.json()["id"]
|
|
response = await client.delete(f"/transcripts/{tid}")
|
|
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):
|
|
response = await client.post("/transcripts", json={"name": "test"})
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "test"
|
|
assert response.json()["reviewed"] is False
|
|
|
|
tid = response.json()["id"]
|
|
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "test"
|
|
assert response.json()["reviewed"] is False
|
|
|
|
response = await client.patch(f"/transcripts/{tid}", json={"reviewed": True})
|
|
assert response.status_code == 200
|
|
assert response.json()["reviewed"] is True
|
|
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["reviewed"] is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_get_returns_room_name(authenticated_client, client):
|
|
"""Test that getting a transcript returns its room_name when linked to a room."""
|
|
# Create a room
|
|
room = await rooms_controller.add(
|
|
name="test-room-for-transcript",
|
|
user_id="test-user",
|
|
zulip_auto_post=False,
|
|
zulip_stream="",
|
|
zulip_topic="",
|
|
is_locked=False,
|
|
room_mode="normal",
|
|
recording_type="cloud",
|
|
recording_trigger="automatic-2nd-participant",
|
|
is_shared=False,
|
|
webhook_url="",
|
|
webhook_secret="",
|
|
)
|
|
|
|
# Create a transcript linked to the room
|
|
transcript = await transcripts_controller.add(
|
|
name="transcript-with-room",
|
|
source_kind="file",
|
|
room_id=room.id,
|
|
)
|
|
|
|
# Get the transcript and verify room_name is returned
|
|
response = await client.get(f"/transcripts/{transcript.id}")
|
|
assert response.status_code == 200
|
|
assert response.json()["room_id"] == room.id
|
|
assert response.json()["room_name"] == "test-room-for-transcript"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_get_returns_null_room_name_when_no_room(
|
|
authenticated_client, client
|
|
):
|
|
"""Test that room_name is null when transcript has no room."""
|
|
response = await client.post("/transcripts", json={"name": "no-room-transcript"})
|
|
assert response.status_code == 200
|
|
tid = response.json()["id"]
|
|
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["room_id"] is None
|
|
assert response.json()["room_name"] is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcripts_list_filtered_by_room_id(authenticated_client, client):
|
|
"""GET /transcripts?room_id=X returns only transcripts for that room."""
|
|
# Use same user as authenticated_client (conftest uses "randomuserid")
|
|
user_id = "randomuserid"
|
|
room = await rooms_controller.add(
|
|
name="room-for-list-filter",
|
|
user_id=user_id,
|
|
zulip_auto_post=False,
|
|
zulip_stream="",
|
|
zulip_topic="",
|
|
is_locked=False,
|
|
room_mode="normal",
|
|
recording_type="cloud",
|
|
recording_trigger="automatic-2nd-participant",
|
|
is_shared=False,
|
|
webhook_url="",
|
|
webhook_secret="",
|
|
)
|
|
in_room = await transcripts_controller.add(
|
|
name="in-room",
|
|
source_kind="file",
|
|
room_id=room.id,
|
|
user_id=user_id,
|
|
)
|
|
other = await transcripts_controller.add(
|
|
name="no-room",
|
|
source_kind="file",
|
|
room_id=None,
|
|
user_id=user_id,
|
|
)
|
|
|
|
response = await client.get("/transcripts", params={"room_id": room.id})
|
|
assert response.status_code == 200
|
|
items = response.json()["items"]
|
|
ids = [t["id"] for t in items]
|
|
assert in_room.id in ids
|
|
assert other.id not in ids
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Restore tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_restore(authenticated_client, client):
|
|
"""Soft-delete then restore, verify accessible again."""
|
|
response = await client.post("/transcripts", json={"name": "restore-me"})
|
|
assert response.status_code == 200
|
|
tid = response.json()["id"]
|
|
|
|
# Soft-delete
|
|
response = await client.delete(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
|
|
# 404 while deleted
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 404
|
|
|
|
# Restore
|
|
response = await client.post(f"/transcripts/{tid}/restore")
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "ok"
|
|
|
|
# Accessible again
|
|
response = await client.get(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "restore-me"
|
|
|
|
# deleted_at is cleared
|
|
transcript = await transcripts_controller.get_by_id(tid)
|
|
assert transcript.deleted_at is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_restore_recording_also_restored(authenticated_client, client):
|
|
"""Restoring a transcript also restores its recording."""
|
|
recording = await recordings_controller.create(
|
|
Recording(
|
|
bucket_name="test-bucket",
|
|
object_key="restore-test.mp4",
|
|
recorded_at=datetime.now(timezone.utc),
|
|
)
|
|
)
|
|
transcript = await transcripts_controller.add(
|
|
name="restore-with-recording",
|
|
source_kind=SourceKind.ROOM,
|
|
recording_id=recording.id,
|
|
user_id="randomuserid",
|
|
)
|
|
|
|
# Soft-delete
|
|
response = await client.delete(f"/transcripts/{transcript.id}")
|
|
assert response.status_code == 200
|
|
|
|
# Both should be soft-deleted
|
|
rec = await recordings_controller.get_by_id(recording.id)
|
|
assert rec.deleted_at is not None
|
|
|
|
# Restore
|
|
response = await client.post(f"/transcripts/{transcript.id}/restore")
|
|
assert response.status_code == 200
|
|
|
|
# Recording also restored
|
|
rec = await recordings_controller.get_by_id(recording.id)
|
|
assert rec.deleted_at is None
|
|
|
|
tr = await transcripts_controller.get_by_id(transcript.id)
|
|
assert tr.deleted_at is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_restore_not_deleted(authenticated_client, client):
|
|
"""Restoring a non-deleted transcript returns 400."""
|
|
response = await client.post("/transcripts", json={"name": "not-deleted"})
|
|
assert response.status_code == 200
|
|
tid = response.json()["id"]
|
|
|
|
response = await client.post(f"/transcripts/{tid}/restore")
|
|
assert response.status_code == 400
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_restore_not_found(authenticated_client, client):
|
|
"""Restoring a nonexistent transcript returns 404."""
|
|
response = await client.post("/transcripts/nonexistent-id/restore")
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_restore_forbidden(authenticated_client, client):
|
|
"""Cannot restore another user's deleted transcript."""
|
|
# Create transcript owned by a different user
|
|
transcript = await transcripts_controller.add(
|
|
name="other-user-restore",
|
|
source_kind=SourceKind.FILE,
|
|
user_id="some-other-user",
|
|
)
|
|
# Soft-delete directly in DB
|
|
await transcripts_controller.remove_by_id(transcript.id, user_id="some-other-user")
|
|
|
|
# Try to restore as randomuserid (authenticated_client)
|
|
response = await client.post(f"/transcripts/{transcript.id}/restore")
|
|
assert response.status_code == 403
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Destroy tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_destroy_storage():
|
|
"""Mock storage backends so hard_delete doesn't require S3 credentials."""
|
|
with (
|
|
patch(
|
|
"reflector.db.transcripts.get_transcripts_storage",
|
|
return_value=AsyncMock(delete_file=AsyncMock()),
|
|
),
|
|
patch(
|
|
"reflector.db.transcripts.get_source_storage",
|
|
return_value=AsyncMock(delete_file=AsyncMock()),
|
|
),
|
|
):
|
|
yield
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_destroy(authenticated_client, client, mock_destroy_storage):
|
|
"""Soft-delete then destroy, verify transcript gone from DB."""
|
|
response = await client.post("/transcripts", json={"name": "destroy-me"})
|
|
assert response.status_code == 200
|
|
tid = response.json()["id"]
|
|
|
|
# Soft-delete first
|
|
response = await client.delete(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
|
|
# Destroy
|
|
response = await client.delete(f"/transcripts/{tid}/destroy")
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "ok"
|
|
|
|
# Gone from DB entirely
|
|
transcript = await transcripts_controller.get_by_id(tid)
|
|
assert transcript is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_destroy_not_soft_deleted(authenticated_client, client):
|
|
"""Cannot destroy a transcript that hasn't been soft-deleted."""
|
|
response = await client.post("/transcripts", json={"name": "not-soft-deleted"})
|
|
assert response.status_code == 200
|
|
tid = response.json()["id"]
|
|
|
|
response = await client.delete(f"/transcripts/{tid}/destroy")
|
|
assert response.status_code == 400
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_destroy_with_recording(
|
|
authenticated_client, client, mock_destroy_storage
|
|
):
|
|
"""Destroying a transcript also hard-deletes its recording from DB."""
|
|
recording = await recordings_controller.create(
|
|
Recording(
|
|
bucket_name="test-bucket",
|
|
object_key="destroy-test.mp4",
|
|
recorded_at=datetime.now(timezone.utc),
|
|
)
|
|
)
|
|
transcript = await transcripts_controller.add(
|
|
name="destroy-with-recording",
|
|
source_kind=SourceKind.ROOM,
|
|
recording_id=recording.id,
|
|
user_id="randomuserid",
|
|
)
|
|
|
|
# Soft-delete
|
|
response = await client.delete(f"/transcripts/{transcript.id}")
|
|
assert response.status_code == 200
|
|
|
|
# Destroy
|
|
response = await client.delete(f"/transcripts/{transcript.id}/destroy")
|
|
assert response.status_code == 200
|
|
|
|
# Both gone from DB
|
|
assert await transcripts_controller.get_by_id(transcript.id) is None
|
|
assert await recordings_controller.get_by_id(recording.id) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_destroy_forbidden(authenticated_client, client):
|
|
"""Cannot destroy another user's deleted transcript."""
|
|
transcript = await transcripts_controller.add(
|
|
name="other-user-destroy",
|
|
source_kind=SourceKind.FILE,
|
|
user_id="some-other-user",
|
|
)
|
|
await transcripts_controller.remove_by_id(transcript.id, user_id="some-other-user")
|
|
|
|
# Try to destroy as randomuserid (authenticated_client)
|
|
response = await client.delete(f"/transcripts/{transcript.id}/destroy")
|
|
assert response.status_code == 403
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Isolation tests — verify unrelated data is NOT deleted
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_destroy_does_not_delete_meeting(
|
|
authenticated_client, client, mock_destroy_storage
|
|
):
|
|
"""Destroying a transcript must NOT delete its associated meeting."""
|
|
room = await rooms_controller.add(
|
|
name="room-for-meeting-isolation",
|
|
user_id="randomuserid",
|
|
zulip_auto_post=False,
|
|
zulip_stream="",
|
|
zulip_topic="",
|
|
is_locked=False,
|
|
room_mode="normal",
|
|
recording_type="cloud",
|
|
recording_trigger="automatic-2nd-participant",
|
|
is_shared=False,
|
|
webhook_url="",
|
|
webhook_secret="",
|
|
)
|
|
now = datetime.now(timezone.utc)
|
|
meeting = await meetings_controller.create(
|
|
id="meeting-isolation-test",
|
|
room_name=room.name,
|
|
room_url="https://example.com/room",
|
|
host_room_url="https://example.com/room-host",
|
|
start_date=now,
|
|
end_date=now + timedelta(hours=1),
|
|
room=room,
|
|
)
|
|
recording = await recordings_controller.create(
|
|
Recording(
|
|
bucket_name="test-bucket",
|
|
object_key="meeting-iso.mp4",
|
|
recorded_at=now,
|
|
meeting_id=meeting.id,
|
|
)
|
|
)
|
|
transcript = await transcripts_controller.add(
|
|
name="transcript-with-meeting",
|
|
source_kind=SourceKind.ROOM,
|
|
recording_id=recording.id,
|
|
meeting_id=meeting.id,
|
|
room_id=room.id,
|
|
user_id="randomuserid",
|
|
)
|
|
|
|
# Soft-delete then destroy
|
|
await transcripts_controller.remove_by_id(transcript.id, user_id="randomuserid")
|
|
response = await client.delete(f"/transcripts/{transcript.id}/destroy")
|
|
assert response.status_code == 200
|
|
|
|
# Transcript and recording are gone
|
|
assert await transcripts_controller.get_by_id(transcript.id) is None
|
|
assert await recordings_controller.get_by_id(recording.id) is None
|
|
|
|
# Meeting still exists
|
|
m = await meetings_controller.get_by_id(meeting.id)
|
|
assert m is not None
|
|
assert m.id == meeting.id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_destroy_does_not_affect_other_transcripts(
|
|
authenticated_client, client, mock_destroy_storage
|
|
):
|
|
"""Destroying one transcript must not affect another transcript or its recording."""
|
|
user_id = "randomuserid"
|
|
rec1 = await recordings_controller.create(
|
|
Recording(
|
|
bucket_name="test-bucket",
|
|
object_key="sibling1.mp4",
|
|
recorded_at=datetime.now(timezone.utc),
|
|
)
|
|
)
|
|
rec2 = await recordings_controller.create(
|
|
Recording(
|
|
bucket_name="test-bucket",
|
|
object_key="sibling2.mp4",
|
|
recorded_at=datetime.now(timezone.utc),
|
|
)
|
|
)
|
|
t1 = await transcripts_controller.add(
|
|
name="sibling-1",
|
|
source_kind=SourceKind.FILE,
|
|
recording_id=rec1.id,
|
|
user_id=user_id,
|
|
)
|
|
t2 = await transcripts_controller.add(
|
|
name="sibling-2",
|
|
source_kind=SourceKind.FILE,
|
|
recording_id=rec2.id,
|
|
user_id=user_id,
|
|
)
|
|
|
|
# Soft-delete and destroy t1
|
|
await transcripts_controller.remove_by_id(t1.id, user_id=user_id)
|
|
response = await client.delete(f"/transcripts/{t1.id}/destroy")
|
|
assert response.status_code == 200
|
|
|
|
# t1 and rec1 gone
|
|
assert await transcripts_controller.get_by_id(t1.id) is None
|
|
assert await recordings_controller.get_by_id(rec1.id) is None
|
|
|
|
# t2 and rec2 untouched
|
|
t2_after = await transcripts_controller.get_by_id(t2.id)
|
|
assert t2_after is not None
|
|
assert t2_after.deleted_at is None
|
|
rec2_after = await recordings_controller.get_by_id(rec2.id)
|
|
assert rec2_after is not None
|
|
assert rec2_after.deleted_at is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transcript_destroy_meeting_with_multiple_transcripts(
|
|
authenticated_client, client, mock_destroy_storage
|
|
):
|
|
"""Destroying one transcript from a meeting must not affect the other
|
|
transcript, its recording, or the shared meeting."""
|
|
user_id = "randomuserid"
|
|
room = await rooms_controller.add(
|
|
name="room-multi-transcript",
|
|
user_id=user_id,
|
|
zulip_auto_post=False,
|
|
zulip_stream="",
|
|
zulip_topic="",
|
|
is_locked=False,
|
|
room_mode="normal",
|
|
recording_type="cloud",
|
|
recording_trigger="automatic-2nd-participant",
|
|
is_shared=False,
|
|
webhook_url="",
|
|
webhook_secret="",
|
|
)
|
|
now = datetime.now(timezone.utc)
|
|
meeting = await meetings_controller.create(
|
|
id="meeting-multi-transcript-test",
|
|
room_name=room.name,
|
|
room_url="https://example.com/room",
|
|
host_room_url="https://example.com/room-host",
|
|
start_date=now,
|
|
end_date=now + timedelta(hours=1),
|
|
room=room,
|
|
)
|
|
rec1 = await recordings_controller.create(
|
|
Recording(
|
|
bucket_name="test-bucket",
|
|
object_key="multi1.mp4",
|
|
recorded_at=now,
|
|
meeting_id=meeting.id,
|
|
)
|
|
)
|
|
rec2 = await recordings_controller.create(
|
|
Recording(
|
|
bucket_name="test-bucket",
|
|
object_key="multi2.mp4",
|
|
recorded_at=now,
|
|
meeting_id=meeting.id,
|
|
)
|
|
)
|
|
t1 = await transcripts_controller.add(
|
|
name="multi-t1",
|
|
source_kind=SourceKind.ROOM,
|
|
recording_id=rec1.id,
|
|
meeting_id=meeting.id,
|
|
room_id=room.id,
|
|
user_id=user_id,
|
|
)
|
|
t2 = await transcripts_controller.add(
|
|
name="multi-t2",
|
|
source_kind=SourceKind.ROOM,
|
|
recording_id=rec2.id,
|
|
meeting_id=meeting.id,
|
|
room_id=room.id,
|
|
user_id=user_id,
|
|
)
|
|
|
|
# Soft-delete and destroy t1
|
|
await transcripts_controller.remove_by_id(t1.id, user_id=user_id)
|
|
response = await client.delete(f"/transcripts/{t1.id}/destroy")
|
|
assert response.status_code == 200
|
|
|
|
# t1 + rec1 gone
|
|
assert await transcripts_controller.get_by_id(t1.id) is None
|
|
assert await recordings_controller.get_by_id(rec1.id) is None
|
|
|
|
# t2 + rec2 + meeting all still exist
|
|
assert (await transcripts_controller.get_by_id(t2.id)) is not None
|
|
assert (await recordings_controller.get_by_id(rec2.id)) is not None
|
|
assert (await meetings_controller.get_by_id(meeting.id)) is not None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Search tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_include_deleted(authenticated_client, client):
|
|
"""Search with include_deleted=true returns only deleted transcripts."""
|
|
response = await client.post("/transcripts", json={"name": "search-deleted"})
|
|
assert response.status_code == 200
|
|
tid = response.json()["id"]
|
|
|
|
# Soft-delete
|
|
response = await client.delete(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
|
|
# Normal search should not include it
|
|
response = await client.get("/transcripts/search", params={"q": ""})
|
|
assert response.status_code == 200
|
|
ids = [r["id"] for r in response.json()["results"]]
|
|
assert tid not in ids
|
|
|
|
# Search with include_deleted should include it
|
|
response = await client.get(
|
|
"/transcripts/search", params={"q": "", "include_deleted": True}
|
|
)
|
|
assert response.status_code == 200
|
|
ids = [r["id"] for r in response.json()["results"]]
|
|
assert tid in ids
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_exclude_deleted_by_default(authenticated_client, client):
|
|
"""Normal search excludes deleted transcripts by default."""
|
|
response = await client.post(
|
|
"/transcripts", json={"name": "search-exclude-deleted"}
|
|
)
|
|
assert response.status_code == 200
|
|
tid = response.json()["id"]
|
|
|
|
# Verify it appears in search
|
|
response = await client.get("/transcripts/search", params={"q": ""})
|
|
assert response.status_code == 200
|
|
ids = [r["id"] for r in response.json()["results"]]
|
|
assert tid in ids
|
|
|
|
# Soft-delete
|
|
response = await client.delete(f"/transcripts/{tid}")
|
|
assert response.status_code == 200
|
|
|
|
# Verify it no longer appears in default search
|
|
response = await client.get("/transcripts/search", params={"q": ""})
|
|
assert response.status_code == 200
|
|
ids = [r["id"] for r in response.json()["results"]]
|
|
assert tid not in ids
|