Files
noteflow/tests/application/test_meeting_service.py
Travis Vasceannie b11633192a
Some checks failed
CI / test-python (push) Failing after 22m14s
CI / test-rust (push) Has been cancelled
CI / test-typescript (push) Has been cancelled
deps
2026-01-24 21:31:58 +00:00

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()