463 lines
17 KiB
Python
463 lines
17 KiB
Python
"""Tests for MeetingService application service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from noteflow.application.services.meeting import MeetingService
|
|
from noteflow.application.services.meeting._meeting_types import SegmentData
|
|
from noteflow.domain.entities import Annotation, Meeting, Segment, Summary
|
|
from noteflow.domain.value_objects import AnnotationId, AnnotationType, MeetingId, MeetingState
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Sequence
|
|
|
|
|
|
class TestMeetingServiceCreation:
|
|
"""Tests for meeting creation operations."""
|
|
|
|
async def test_create_meeting_success(
|
|
self,
|
|
sample_meeting: Meeting,
|
|
mock_uow: MagicMock,
|
|
) -> None:
|
|
"""Test successful meeting creation."""
|
|
mock_uow.meetings.create = AsyncMock(return_value=sample_meeting)
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.create_meeting(title=sample_meeting.title)
|
|
|
|
assert result.title == sample_meeting.title
|
|
mock_uow.meetings.create.assert_called_once()
|
|
mock_uow.commit.assert_called_once()
|
|
|
|
async def test_create_meeting_with_metadata(self, mock_uow: MagicMock) -> None:
|
|
"""Test meeting creation with metadata."""
|
|
metadata = {"project": "NoteFlow"}
|
|
created_meeting = Meeting.create(title="Test", metadata=metadata)
|
|
mock_uow.meetings.create = AsyncMock(return_value=created_meeting)
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.create_meeting(title="Test", metadata=metadata)
|
|
|
|
assert result.metadata == metadata
|
|
|
|
|
|
class TestMeetingServiceRetrieval:
|
|
"""Tests for meeting retrieval operations."""
|
|
|
|
async def test_get_meeting_found(self, mock_uow: MagicMock) -> None:
|
|
"""Test retrieving existing meeting."""
|
|
meeting_id = MeetingId(uuid4())
|
|
expected_meeting = Meeting.create(title="Found")
|
|
mock_uow.meetings.get = AsyncMock(return_value=expected_meeting)
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.get_meeting(meeting_id)
|
|
|
|
assert result is not None, "Expected meeting to be found"
|
|
assert result.title == "Found", "Meeting title should match expected value"
|
|
|
|
async def test_get_meeting_not_found(self, mock_uow: MagicMock) -> None:
|
|
"""Test retrieving non-existent meeting."""
|
|
meeting_id = MeetingId(uuid4())
|
|
mock_uow.meetings.get = AsyncMock(return_value=None)
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.get_meeting(meeting_id)
|
|
|
|
assert result is None
|
|
|
|
async def test_list_meetings(self, mock_uow: MagicMock) -> None:
|
|
"""Test listing meetings with pagination."""
|
|
meetings: Sequence[Meeting] = [
|
|
Meeting.create(title="Meeting 1"),
|
|
Meeting.create(title="Meeting 2"),
|
|
]
|
|
mock_uow.meetings.list_all = AsyncMock(return_value=(meetings, 10))
|
|
|
|
service = MeetingService(mock_uow)
|
|
result, total = await service.list_meetings(limit=2, offset=0)
|
|
|
|
assert len(result) == 2, "Should return requested number of meetings"
|
|
assert total == 10, "Total count should reflect all available meetings"
|
|
mock_uow.meetings.list_all.assert_called_once_with(
|
|
states=None, limit=2, offset=0, sort_desc=True
|
|
)
|
|
|
|
|
|
class TestMeetingServiceStateTransitions:
|
|
"""Tests for meeting state transition operations."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("initial_state", "action_method", "expected_state"),
|
|
[
|
|
pytest.param(
|
|
MeetingState.CREATED,
|
|
"start_recording",
|
|
MeetingState.RECORDING,
|
|
id="created-to-recording",
|
|
),
|
|
pytest.param(
|
|
MeetingState.RECORDING,
|
|
"stop_meeting",
|
|
MeetingState.STOPPED,
|
|
id="recording-to-stopped",
|
|
),
|
|
pytest.param(
|
|
MeetingState.STOPPED,
|
|
"complete_meeting",
|
|
MeetingState.COMPLETED,
|
|
id="stopped-to-completed",
|
|
),
|
|
],
|
|
)
|
|
async def test_valid_state_transitions(
|
|
self,
|
|
initial_state: MeetingState,
|
|
action_method: str,
|
|
expected_state: MeetingState,
|
|
mock_uow: MagicMock,
|
|
) -> None:
|
|
"""Test valid state transitions succeed and commit."""
|
|
meeting = Meeting.create(title="Test")
|
|
meeting.state = initial_state
|
|
|
|
mock_uow.meetings.get = AsyncMock(return_value=meeting)
|
|
mock_uow.meetings.update = AsyncMock(return_value=meeting)
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await getattr(service, action_method)(meeting.id)
|
|
|
|
assert result is not None, f"Expected result for {action_method}"
|
|
assert result.state == expected_state, (
|
|
f"Expected state {expected_state} after {action_method}"
|
|
)
|
|
mock_uow.commit.assert_called_once()
|
|
|
|
@pytest.mark.parametrize(
|
|
("initial_state", "action_method", "error_action"),
|
|
[
|
|
pytest.param(
|
|
MeetingState.CREATED, "stop_meeting", "begin stopping", id="cannot-stop-created"
|
|
),
|
|
pytest.param(
|
|
MeetingState.COMPLETED,
|
|
"start_recording",
|
|
"start recording",
|
|
id="cannot-start-completed",
|
|
),
|
|
],
|
|
)
|
|
async def test_meeting_service_invalid_state_transitions_raise(
|
|
self,
|
|
initial_state: MeetingState,
|
|
action_method: str,
|
|
error_action: str,
|
|
mock_uow: MagicMock,
|
|
) -> None:
|
|
"""Test invalid state transitions raise ValueError and do not commit."""
|
|
meeting = Meeting.create(title="Test")
|
|
meeting.state = initial_state
|
|
|
|
mock_uow.meetings.get = AsyncMock(return_value=meeting)
|
|
mock_uow.meetings.update = AsyncMock(return_value=meeting)
|
|
|
|
service = MeetingService(mock_uow)
|
|
|
|
with pytest.raises(ValueError, match=f"Cannot {error_action} from state"):
|
|
await getattr(service, action_method)(meeting.id)
|
|
mock_uow.commit.assert_not_called()
|
|
|
|
|
|
class TestMeetingServiceDeletion:
|
|
"""Tests for meeting deletion operations."""
|
|
|
|
async def test_delete_meeting_success(self, mock_uow: MagicMock) -> None:
|
|
"""Test successful meeting deletion."""
|
|
meeting_id = MeetingId(uuid4())
|
|
mock_meeting = Meeting.create(title="Test Meeting")
|
|
mock_uow.meetings.get = AsyncMock(return_value=mock_meeting)
|
|
mock_uow.meetings.delete = AsyncMock(return_value=True)
|
|
mock_uow.assets.delete_meeting_assets = AsyncMock()
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.delete_meeting(meeting_id)
|
|
|
|
assert result is True
|
|
mock_uow.assets.delete_meeting_assets.assert_called_once_with(
|
|
meeting_id, mock_meeting.asset_path
|
|
)
|
|
mock_uow.meetings.delete.assert_called_once_with(meeting_id)
|
|
mock_uow.commit.assert_called_once()
|
|
|
|
async def test_delete_meeting_not_found(self, mock_uow: MagicMock) -> None:
|
|
"""Test deleting non-existent meeting returns False."""
|
|
meeting_id = MeetingId(uuid4())
|
|
mock_uow.meetings.get = AsyncMock(return_value=None)
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.delete_meeting(meeting_id)
|
|
|
|
assert result is False
|
|
mock_uow.assets.delete_meeting_assets.assert_not_called()
|
|
mock_uow.meetings.delete.assert_not_called()
|
|
mock_uow.commit.assert_not_called()
|
|
|
|
|
|
class TestMeetingServiceSegments:
|
|
"""Tests for segment operations."""
|
|
|
|
async def test_add_segment_success(self, mock_uow: MagicMock) -> None:
|
|
"""Test adding a segment to meeting."""
|
|
meeting_id = MeetingId(uuid4())
|
|
segment = Segment(
|
|
segment_id=0, text="Hello", start_time=0.0, end_time=1.0, meeting_id=meeting_id
|
|
)
|
|
mock_uow.segments.add = AsyncMock(return_value=segment)
|
|
|
|
service = MeetingService(mock_uow)
|
|
segment_data = SegmentData(
|
|
segment_id=0,
|
|
text="Hello",
|
|
start_time=0.0,
|
|
end_time=1.0,
|
|
)
|
|
result = await service.add_segment(meeting_id=meeting_id, data=segment_data)
|
|
|
|
assert result.text == "Hello"
|
|
mock_uow.segments.add.assert_called_once()
|
|
mock_uow.commit.assert_called_once()
|
|
|
|
async def test_get_segments(self, mock_uow: MagicMock) -> None:
|
|
"""Test retrieving segments for meeting."""
|
|
meeting_id = MeetingId(uuid4())
|
|
segments: Sequence[Segment] = [
|
|
Segment(segment_id=0, text="First", start_time=0.0, end_time=1.0),
|
|
Segment(segment_id=1, text="Second", start_time=1.0, end_time=2.0),
|
|
]
|
|
mock_uow.segments.get_by_meeting = AsyncMock(return_value=segments)
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.get_segments(meeting_id)
|
|
|
|
assert len(result) == 2
|
|
mock_uow.segments.get_by_meeting.assert_called_once_with(meeting_id, include_words=True)
|
|
|
|
async def test_add_segments_batch(self, mock_uow: MagicMock) -> None:
|
|
"""Test batch adding segments commits once."""
|
|
meeting_id = MeetingId(uuid4())
|
|
segments = [
|
|
Segment(segment_id=0, text="A", start_time=0.0, end_time=1.0),
|
|
Segment(segment_id=1, text="B", start_time=1.0, end_time=2.0),
|
|
]
|
|
mock_uow.segments.add_batch = AsyncMock(return_value=segments)
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.add_segments_batch(meeting_id=meeting_id, segments=segments)
|
|
|
|
assert len(result) == 2
|
|
mock_uow.segments.add_batch.assert_called_once_with(meeting_id, segments)
|
|
mock_uow.commit.assert_called_once()
|
|
|
|
|
|
class TestMeetingServiceSummaries:
|
|
"""Tests for summary operations."""
|
|
|
|
async def test_save_summary_success(self, mock_uow: MagicMock) -> None:
|
|
"""Test saving a meeting summary."""
|
|
meeting_id = MeetingId(uuid4())
|
|
summary = Summary(
|
|
meeting_id=meeting_id,
|
|
executive_summary="Test summary",
|
|
generated_at=datetime.now(UTC),
|
|
provider_name="test",
|
|
model_name="v1",
|
|
)
|
|
mock_uow.summaries.save = AsyncMock(return_value=summary)
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.save_summary(
|
|
meeting_id=meeting_id,
|
|
executive_summary="Test summary",
|
|
provider_name="test",
|
|
model_name="v1",
|
|
)
|
|
|
|
assert result.executive_summary == "Test summary"
|
|
mock_uow.summaries.save.assert_called_once()
|
|
mock_uow.commit.assert_called_once()
|
|
|
|
async def test_get_summary_found(self, mock_uow: MagicMock) -> None:
|
|
"""Test retrieving existing summary."""
|
|
meeting_id = MeetingId(uuid4())
|
|
summary = Summary(meeting_id=meeting_id, executive_summary="Found")
|
|
mock_uow.summaries.get_by_meeting = AsyncMock(return_value=summary)
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.fetch_meeting_summary(meeting_id)
|
|
|
|
assert result is not None, "Expected summary to be found"
|
|
assert result.executive_summary == "Found", (
|
|
"Summary executive_summary should match expected value"
|
|
)
|
|
|
|
async def test_get_summary_not_found(self, mock_uow: MagicMock) -> None:
|
|
"""Test retrieving non-existent summary."""
|
|
meeting_id = MeetingId(uuid4())
|
|
mock_uow.summaries.get_by_meeting = AsyncMock(return_value=None)
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.fetch_meeting_summary(meeting_id)
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestMeetingServiceSearch:
|
|
"""Tests for semantic search operations."""
|
|
|
|
async def test_search_segments_delegates(self, mock_uow: MagicMock) -> None:
|
|
"""Test search_segments delegates to repository."""
|
|
meeting_id = MeetingId(uuid4())
|
|
segment = Segment(segment_id=0, text="A", start_time=0.0, end_time=1.0)
|
|
mock_uow.segments.search_semantic = AsyncMock(return_value=[(segment, 0.9)])
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.search_segments(query_embedding=[0.1], meeting_id=meeting_id)
|
|
|
|
assert len(result) == 1
|
|
mock_uow.segments.search_semantic.assert_called_once_with(
|
|
query_embedding=[0.1], limit=10, meeting_id=meeting_id
|
|
)
|
|
|
|
|
|
class TestMeetingServiceAnnotations:
|
|
"""Tests for annotation operations."""
|
|
|
|
async def test_add_annotation_success(self, mock_uow: MagicMock) -> None:
|
|
"""Test adding an annotation commits and returns saved entity."""
|
|
meeting_id = MeetingId(uuid4())
|
|
mock_uow.annotations.add = AsyncMock()
|
|
|
|
service = MeetingService(mock_uow)
|
|
await service.add_annotation(
|
|
meeting_id=meeting_id,
|
|
annotation_type=AnnotationType.NOTE,
|
|
text="Note",
|
|
start_time=0.0,
|
|
end_time=1.0,
|
|
)
|
|
|
|
mock_uow.annotations.add.assert_called_once()
|
|
mock_uow.commit.assert_called_once()
|
|
|
|
async def test_get_annotations_in_range(self, mock_uow: MagicMock) -> None:
|
|
"""Test get_annotations_in_range delegates to repository."""
|
|
meeting_id = MeetingId(uuid4())
|
|
mock_uow.annotations.get_by_time_range = AsyncMock(return_value=[])
|
|
|
|
service = MeetingService(mock_uow)
|
|
await service.get_annotations_in_range(meeting_id, start_time=1.0, end_time=2.0)
|
|
|
|
mock_uow.annotations.get_by_time_range.assert_called_once_with(meeting_id, 1.0, 2.0)
|
|
|
|
async def test_update_annotation_not_found_raises(self, mock_uow: MagicMock) -> None:
|
|
"""Test update_annotation propagates repository errors."""
|
|
meeting_id = MeetingId(uuid4())
|
|
annotation = Annotation(
|
|
id=AnnotationId(uuid4()),
|
|
meeting_id=meeting_id,
|
|
annotation_type=AnnotationType.NOTE,
|
|
text="Note",
|
|
start_time=0.0,
|
|
end_time=1.0,
|
|
)
|
|
mock_uow.annotations.update = AsyncMock(side_effect=ValueError("Annotation not found"))
|
|
|
|
service = MeetingService(mock_uow)
|
|
with pytest.raises(ValueError, match="Annotation not found"):
|
|
await service.update_annotation(annotation)
|
|
|
|
mock_uow.commit.assert_not_called()
|
|
|
|
async def test_delete_annotation_not_found(self, mock_uow: MagicMock) -> None:
|
|
"""Test delete_annotation returns False when missing."""
|
|
annotation_id = AnnotationId(uuid4())
|
|
mock_uow.annotations.delete = AsyncMock(return_value=False)
|
|
|
|
service = MeetingService(mock_uow)
|
|
result = await service.delete_annotation(annotation_id)
|
|
|
|
assert result is False
|
|
mock_uow.commit.assert_not_called()
|
|
|
|
|
|
class TestMeetingServiceAdditionalBranches:
|
|
"""Additional branch coverage for MeetingService."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"method_name",
|
|
[
|
|
pytest.param("start_recording", id="start_recording"),
|
|
pytest.param("stop_meeting", id="stop_meeting"),
|
|
pytest.param("complete_meeting", id="complete_meeting"),
|
|
],
|
|
)
|
|
async def test_meeting_operation_not_found(
|
|
self,
|
|
mock_uow: MagicMock,
|
|
method_name: str,
|
|
) -> None:
|
|
"""Meeting operations should return None when meeting is missing."""
|
|
mock_uow.meetings.get = AsyncMock(return_value=None)
|
|
service = MeetingService(mock_uow)
|
|
|
|
result = await getattr(service, method_name)(MeetingId(uuid4()))
|
|
|
|
assert result is None
|
|
mock_uow.commit.assert_not_called()
|
|
|
|
async def test_get_annotation_delegates_repository(self, mock_uow: MagicMock) -> None:
|
|
"""get_annotation should delegate to repository."""
|
|
annotation = Annotation(
|
|
id=AnnotationId(uuid4()),
|
|
meeting_id=MeetingId(uuid4()),
|
|
annotation_type=AnnotationType.NOTE,
|
|
text="note",
|
|
start_time=0.0,
|
|
end_time=1.0,
|
|
)
|
|
mock_uow.annotations.get = AsyncMock(return_value=annotation)
|
|
service = MeetingService(mock_uow)
|
|
|
|
result = await service.get_annotation(annotation.id)
|
|
|
|
assert result is annotation
|
|
mock_uow.annotations.get.assert_called_once_with(annotation.id)
|
|
|
|
async def test_get_annotations_delegates_repository(self, mock_uow: MagicMock) -> None:
|
|
"""get_annotations should delegate to repository."""
|
|
meeting_id = MeetingId(uuid4())
|
|
mock_uow.annotations.get_by_meeting = AsyncMock(return_value=[])
|
|
service = MeetingService(mock_uow)
|
|
|
|
await service.get_annotations(meeting_id)
|
|
|
|
mock_uow.annotations.get_by_meeting.assert_called_once_with(meeting_id)
|
|
|
|
async def test_delete_annotation_success_commits(self, mock_uow: MagicMock) -> None:
|
|
"""delete_annotation should commit on success."""
|
|
annotation_id = AnnotationId(uuid4())
|
|
mock_uow.annotations.delete = AsyncMock(return_value=True)
|
|
service = MeetingService(mock_uow)
|
|
|
|
result = await service.delete_annotation(annotation_id)
|
|
|
|
assert result is True
|
|
mock_uow.commit.assert_called_once()
|