728 lines
24 KiB
Python
728 lines
24 KiB
Python
"""Tests for EntitiesMixin gRPC endpoints.
|
|
|
|
Tests cover:
|
|
- ExtractEntities: extraction with entities returned, meeting not found, NER disabled
|
|
- UpdateEntity: success, entity not found, invalid entity_id format
|
|
- DeleteEntity: success, entity not found, invalid entity_id format
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from noteflow.application.services.ner import ExtractionResult
|
|
from noteflow.domain.entities.named_entity import EntityCategory, NamedEntity
|
|
from noteflow.domain.value_objects import MeetingId
|
|
from noteflow.grpc.mixins._types import GrpcContext
|
|
from noteflow.grpc.mixins.entities import EntitiesMixin
|
|
from noteflow.grpc.proto import noteflow_pb2
|
|
|
|
if TYPE_CHECKING:
|
|
pass
|
|
|
|
|
|
class MockRepositoryProvider:
|
|
"""Mock repository provider as async context manager."""
|
|
|
|
def __init__(
|
|
self,
|
|
entities_repo: AsyncMock,
|
|
meetings_repo: AsyncMock | None = None,
|
|
) -> None:
|
|
"""Initialize with mock entity repository."""
|
|
self.supports_entities = True
|
|
self.entities = entities_repo
|
|
self.meetings = meetings_repo or AsyncMock()
|
|
self.commit = AsyncMock()
|
|
|
|
async def __aenter__(self) -> MockRepositoryProvider:
|
|
"""Enter async context."""
|
|
return self
|
|
|
|
async def __aexit__(
|
|
self,
|
|
exc_type: type[BaseException] | None,
|
|
exc_val: BaseException | None,
|
|
exc_tb: object,
|
|
) -> None:
|
|
"""Exit async context."""
|
|
|
|
|
|
class MockServicerHost(EntitiesMixin):
|
|
"""Mock servicer host implementing required protocol."""
|
|
|
|
def __init__(
|
|
self,
|
|
entities_repo: AsyncMock,
|
|
meetings_repo: AsyncMock | None = None,
|
|
ner_service: AsyncMock | None = None,
|
|
) -> None:
|
|
"""Initialize with mock repositories and optional NER service."""
|
|
self._entities_repo = entities_repo
|
|
self._meetings_repo = meetings_repo or AsyncMock()
|
|
self.ner_service = ner_service
|
|
# Required by log_model_status in _model_status.py
|
|
self.asr_engine = None
|
|
self.diarization_engine = None
|
|
self.summarization_service = None
|
|
|
|
def create_repository_provider(self) -> MockRepositoryProvider:
|
|
"""Create mock repository provider context manager."""
|
|
return MockRepositoryProvider(self._entities_repo, self._meetings_repo)
|
|
|
|
# Type stubs for mixin methods to fix type inference
|
|
if TYPE_CHECKING:
|
|
|
|
async def ExtractEntities(
|
|
self,
|
|
request: noteflow_pb2.ExtractEntitiesRequest,
|
|
context: GrpcContext,
|
|
) -> noteflow_pb2.ExtractEntitiesResponse: ...
|
|
|
|
async def UpdateEntity(
|
|
self,
|
|
request: noteflow_pb2.UpdateEntityRequest,
|
|
context: GrpcContext,
|
|
) -> noteflow_pb2.UpdateEntityResponse: ...
|
|
|
|
async def DeleteEntity(
|
|
self,
|
|
request: noteflow_pb2.DeleteEntityRequest,
|
|
context: GrpcContext,
|
|
) -> noteflow_pb2.DeleteEntityResponse: ...
|
|
|
|
|
|
def create_sample_entity(
|
|
meeting_id: MeetingId | None = None,
|
|
text: str = "Test Entity",
|
|
category: EntityCategory = EntityCategory.PERSON,
|
|
is_pinned: bool = False,
|
|
) -> NamedEntity:
|
|
"""Create a sample NamedEntity for testing."""
|
|
return NamedEntity(
|
|
id=uuid4(),
|
|
meeting_id=meeting_id,
|
|
text=text,
|
|
normalized_text=text.lower(),
|
|
category=category,
|
|
segment_ids=[0, 1, 2],
|
|
confidence=0.95,
|
|
is_pinned=is_pinned,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_entities_repo() -> AsyncMock:
|
|
"""Create mock entity repository with common methods."""
|
|
repo = AsyncMock()
|
|
repo.get = AsyncMock(return_value=None)
|
|
repo.update = AsyncMock(return_value=None)
|
|
repo.delete = AsyncMock(return_value=False)
|
|
return repo
|
|
|
|
|
|
@pytest.fixture
|
|
def mockner_service() -> MagicMock:
|
|
"""Create mock NER service."""
|
|
service = MagicMock()
|
|
service.extract_entities = AsyncMock()
|
|
service.is_ner_ready = MagicMock(return_value=True)
|
|
return service
|
|
|
|
|
|
class TestExtractEntities:
|
|
"""Tests for ExtractEntities RPC."""
|
|
|
|
@pytest.fixture
|
|
def servicer(
|
|
self,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_meetings_repo: AsyncMock,
|
|
mockner_service: MagicMock,
|
|
) -> MockServicerHost:
|
|
"""Create servicer with mock NER service."""
|
|
return MockServicerHost(
|
|
mock_entities_repo,
|
|
mock_meetings_repo,
|
|
mockner_service,
|
|
)
|
|
|
|
async def test_returns_extracted_entities(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mockner_service: MagicMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""ExtractEntities returns entities from NER service."""
|
|
meeting_id = MeetingId(uuid4())
|
|
entity1 = create_sample_entity(meeting_id, "John Doe", EntityCategory.PERSON)
|
|
entity2 = create_sample_entity(meeting_id, "Acme Corp", EntityCategory.COMPANY)
|
|
|
|
mockner_service.extract_entities.return_value = ExtractionResult(
|
|
entities=[entity1, entity2],
|
|
cached=False,
|
|
total_count=2,
|
|
)
|
|
|
|
request = noteflow_pb2.ExtractEntitiesRequest(
|
|
meeting_id=str(meeting_id),
|
|
force_refresh=False,
|
|
)
|
|
response = await servicer.ExtractEntities(request, mock_grpc_context)
|
|
|
|
assert len(response.entities) == 2, "should return 2 entities"
|
|
assert response.total_count == 2, "total count should be 2"
|
|
assert response.cached is False, "should not be cached"
|
|
|
|
assert response.entities[0].text == "John Doe", "first entity text should match"
|
|
assert response.entities[0].category == "person", "first entity category should be person"
|
|
assert response.entities[1].text == "Acme Corp", "second entity text should match"
|
|
assert response.entities[1].category == "company", (
|
|
"second entity category should be company"
|
|
)
|
|
|
|
async def test_returns_cached_entities(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mockner_service: MagicMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""ExtractEntities returns cached result when available."""
|
|
meeting_id = MeetingId(uuid4())
|
|
entity = create_sample_entity(meeting_id, "Cached Person", EntityCategory.PERSON)
|
|
|
|
mockner_service.extract_entities.return_value = ExtractionResult(
|
|
entities=[entity],
|
|
cached=True,
|
|
total_count=1,
|
|
)
|
|
|
|
request = noteflow_pb2.ExtractEntitiesRequest(
|
|
meeting_id=str(meeting_id),
|
|
force_refresh=False,
|
|
)
|
|
response = await servicer.ExtractEntities(request, mock_grpc_context)
|
|
|
|
assert response.cached is True, "should indicate cached result"
|
|
assert len(response.entities) == 1, "should return 1 cached entity"
|
|
mockner_service.extract_entities.assert_called_once_with(
|
|
meeting_id=meeting_id,
|
|
force_refresh=False,
|
|
)
|
|
|
|
async def test_force_refresh_bypasses_cache(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mockner_service: MagicMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""ExtractEntities with force_refresh re-extracts entities."""
|
|
meeting_id = MeetingId(uuid4())
|
|
entity = create_sample_entity(meeting_id, "Fresh Entity", EntityCategory.PERSON)
|
|
|
|
mockner_service.extract_entities.return_value = ExtractionResult(
|
|
entities=[entity],
|
|
cached=False,
|
|
total_count=1,
|
|
)
|
|
|
|
request = noteflow_pb2.ExtractEntitiesRequest(
|
|
meeting_id=str(meeting_id),
|
|
force_refresh=True,
|
|
)
|
|
response = await servicer.ExtractEntities(request, mock_grpc_context)
|
|
|
|
assert response.cached is False, "should not be cached with force_refresh"
|
|
mockner_service.extract_entities.assert_called_once_with(
|
|
meeting_id=meeting_id,
|
|
force_refresh=True,
|
|
)
|
|
|
|
async def test_aborts_when_meeting_not_found(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mockner_service: MagicMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""ExtractEntities aborts when meeting does not exist."""
|
|
meeting_id = str(uuid4())
|
|
mockner_service.extract_entities.side_effect = ValueError("Meeting not found")
|
|
|
|
request = noteflow_pb2.ExtractEntitiesRequest(
|
|
meeting_id=meeting_id,
|
|
force_refresh=False,
|
|
)
|
|
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await servicer.ExtractEntities(request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|
|
|
|
async def test_aborts_whenner_service_not_configured(
|
|
self,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_meetings_repo: AsyncMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""ExtractEntities aborts when NER service is None."""
|
|
servicer = MockServicerHost(
|
|
mock_entities_repo,
|
|
mock_meetings_repo,
|
|
ner_service=None,
|
|
)
|
|
|
|
request = noteflow_pb2.ExtractEntitiesRequest(
|
|
meeting_id=str(uuid4()),
|
|
force_refresh=False,
|
|
)
|
|
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await servicer.ExtractEntities(request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|
|
|
|
async def test_aborts_when_feature_disabled(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mockner_service: MagicMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""ExtractEntities aborts when NER feature is disabled."""
|
|
mockner_service.extract_entities.side_effect = RuntimeError(
|
|
"NER extraction is disabled by feature flag"
|
|
)
|
|
|
|
request = noteflow_pb2.ExtractEntitiesRequest(
|
|
meeting_id=str(uuid4()),
|
|
force_refresh=False,
|
|
)
|
|
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await servicer.ExtractEntities(request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|
|
|
|
async def test_extract_aborts_with_invalid_meeting_id_format(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""ExtractEntities aborts with invalid meeting_id format."""
|
|
request = noteflow_pb2.ExtractEntitiesRequest(
|
|
meeting_id="not-a-uuid",
|
|
force_refresh=False,
|
|
)
|
|
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await servicer.ExtractEntities(request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|
|
|
|
async def test_returns_empty_entities_when_none_found(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mockner_service: MagicMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""ExtractEntities returns empty list when no entities extracted."""
|
|
mockner_service.extract_entities.return_value = ExtractionResult(
|
|
entities=[],
|
|
cached=False,
|
|
total_count=0,
|
|
)
|
|
|
|
request = noteflow_pb2.ExtractEntitiesRequest(
|
|
meeting_id=str(uuid4()),
|
|
force_refresh=False,
|
|
)
|
|
response = await servicer.ExtractEntities(request, mock_grpc_context)
|
|
|
|
assert len(response.entities) == 0, "should return empty list"
|
|
assert response.total_count == 0, "total count should be 0"
|
|
|
|
async def test_includes_pinned_status_in_response(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mockner_service: MagicMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""ExtractEntities includes is_pinned status for each entity."""
|
|
meeting_id = MeetingId(uuid4())
|
|
pinned_entity = create_sample_entity(
|
|
meeting_id, "Important Person", EntityCategory.PERSON, is_pinned=True
|
|
)
|
|
|
|
mockner_service.extract_entities.return_value = ExtractionResult(
|
|
entities=[pinned_entity],
|
|
cached=True,
|
|
total_count=1,
|
|
)
|
|
|
|
request = noteflow_pb2.ExtractEntitiesRequest(
|
|
meeting_id=str(meeting_id),
|
|
force_refresh=False,
|
|
)
|
|
response = await servicer.ExtractEntities(request, mock_grpc_context)
|
|
|
|
assert response.entities[0].is_pinned is True, "entity should be marked as pinned"
|
|
|
|
|
|
class TestUpdateEntity:
|
|
"""Tests for UpdateEntity RPC."""
|
|
|
|
@pytest.fixture
|
|
def servicer(
|
|
self,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_meetings_repo: AsyncMock,
|
|
) -> MockServicerHost:
|
|
"""Create servicer with mock repositories."""
|
|
return MockServicerHost(mock_entities_repo, mock_meetings_repo)
|
|
|
|
async def test_updates_entity_text(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""UpdateEntity successfully updates entity text."""
|
|
meeting_id = MeetingId(uuid4())
|
|
entity = create_sample_entity(meeting_id, "Original Name", EntityCategory.PERSON)
|
|
|
|
updated_entity = create_sample_entity(meeting_id, "Updated Name", EntityCategory.PERSON)
|
|
updated_entity.id = entity.id
|
|
|
|
mock_entities_repo.get.return_value = entity
|
|
mock_entities_repo.update.return_value = updated_entity
|
|
|
|
request = noteflow_pb2.UpdateEntityRequest(
|
|
meeting_id=str(meeting_id),
|
|
entity_id=str(entity.id),
|
|
text="Updated Name",
|
|
)
|
|
response = await servicer.UpdateEntity(request, mock_grpc_context)
|
|
|
|
assert response.entity.text == "Updated Name", "entity text should be updated"
|
|
mock_entities_repo.update.assert_called_once_with(
|
|
entity_id=entity.id,
|
|
text="Updated Name",
|
|
category=None,
|
|
)
|
|
|
|
async def test_updates_entity_category(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""UpdateEntity successfully updates entity category."""
|
|
meeting_id = MeetingId(uuid4())
|
|
entity = create_sample_entity(meeting_id, "Test Name", EntityCategory.PERSON)
|
|
|
|
updated_entity = create_sample_entity(meeting_id, "Test Name", EntityCategory.COMPANY)
|
|
updated_entity.id = entity.id
|
|
|
|
mock_entities_repo.get.return_value = entity
|
|
mock_entities_repo.update.return_value = updated_entity
|
|
|
|
request = noteflow_pb2.UpdateEntityRequest(
|
|
meeting_id=str(meeting_id),
|
|
entity_id=str(entity.id),
|
|
category="company",
|
|
)
|
|
response = await servicer.UpdateEntity(request, mock_grpc_context)
|
|
|
|
assert response.entity.category == "company", "entity category should be updated"
|
|
|
|
async def test_update_aborts_when_entity_belongs_to_different_meeting(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""UpdateEntity aborts when entity belongs to a different meeting."""
|
|
entity_meeting_id = MeetingId(uuid4())
|
|
request_meeting_id = MeetingId(uuid4())
|
|
entity = create_sample_entity(entity_meeting_id, "Test", EntityCategory.PERSON)
|
|
|
|
mock_entities_repo.get.return_value = entity
|
|
|
|
request = noteflow_pb2.UpdateEntityRequest(
|
|
meeting_id=str(request_meeting_id),
|
|
entity_id=str(entity.id),
|
|
text="New Text",
|
|
)
|
|
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await servicer.UpdateEntity(request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|
|
|
|
@pytest.mark.parametrize(
|
|
("meeting_id", "entity_id"),
|
|
[
|
|
pytest.param("not-a-uuid", str(uuid4()), id="invalid_meeting_id"),
|
|
pytest.param(str(uuid4()), "not-a-uuid", id="invalid_entity_id"),
|
|
],
|
|
)
|
|
async def test_update_aborts_with_invalid_id_format(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mock_grpc_context: MagicMock,
|
|
meeting_id: str,
|
|
entity_id: str,
|
|
) -> None:
|
|
request = noteflow_pb2.UpdateEntityRequest(
|
|
meeting_id=meeting_id,
|
|
entity_id=entity_id,
|
|
text="New Text",
|
|
)
|
|
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await servicer.UpdateEntity(request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|
|
|
|
async def test_aborts_with_invalid_category(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""UpdateEntity aborts with invalid category value."""
|
|
meeting_id = MeetingId(uuid4())
|
|
entity = create_sample_entity(meeting_id, "Test", EntityCategory.PERSON)
|
|
|
|
mock_entities_repo.get.return_value = entity
|
|
mock_entities_repo.update.side_effect = ValueError("Invalid category")
|
|
|
|
request = noteflow_pb2.UpdateEntityRequest(
|
|
meeting_id=str(meeting_id),
|
|
entity_id=str(entity.id),
|
|
category="invalid_category",
|
|
)
|
|
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await servicer.UpdateEntity(request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|
|
|
|
|
|
# ============================================================================
|
|
# TestEntityNotFound
|
|
# ============================================================================
|
|
|
|
|
|
class TestEntityNotFound:
|
|
"""Tests for entity operations when entity is missing."""
|
|
|
|
@pytest.fixture
|
|
def servicer(
|
|
self,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_meetings_repo: AsyncMock,
|
|
) -> MockServicerHost:
|
|
"""Create servicer with mock repositories."""
|
|
return MockServicerHost(mock_entities_repo, mock_meetings_repo)
|
|
|
|
@pytest.mark.parametrize(
|
|
("method_name", "proto_request"),
|
|
[
|
|
pytest.param(
|
|
"UpdateEntity",
|
|
noteflow_pb2.UpdateEntityRequest(
|
|
meeting_id=str(uuid4()),
|
|
entity_id=str(uuid4()),
|
|
text="New Text",
|
|
),
|
|
id="update",
|
|
),
|
|
pytest.param(
|
|
"DeleteEntity",
|
|
noteflow_pb2.DeleteEntityRequest(
|
|
meeting_id=str(uuid4()),
|
|
entity_id=str(uuid4()),
|
|
),
|
|
id="delete",
|
|
),
|
|
],
|
|
)
|
|
async def test_entity_not_found_aborts(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_grpc_context: MagicMock,
|
|
method_name: str,
|
|
proto_request: object,
|
|
) -> None:
|
|
"""Entity operations abort when entity does not exist."""
|
|
mock_entities_repo.get.return_value = None
|
|
|
|
method = getattr(servicer, method_name)
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await method(proto_request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|
|
|
|
|
|
class TestDeleteEntity:
|
|
"""Tests for DeleteEntity RPC."""
|
|
|
|
@pytest.fixture
|
|
def servicer(
|
|
self,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_meetings_repo: AsyncMock,
|
|
) -> MockServicerHost:
|
|
"""Create servicer with mock repositories."""
|
|
return MockServicerHost(mock_entities_repo, mock_meetings_repo)
|
|
|
|
async def test_deletes_entity_successfully(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""DeleteEntity successfully removes entity."""
|
|
meeting_id = MeetingId(uuid4())
|
|
entity = create_sample_entity(meeting_id, "To Delete", EntityCategory.PERSON)
|
|
|
|
mock_entities_repo.get.return_value = entity
|
|
mock_entities_repo.delete.return_value = True
|
|
|
|
request = noteflow_pb2.DeleteEntityRequest(
|
|
meeting_id=str(meeting_id),
|
|
entity_id=str(entity.id),
|
|
)
|
|
response = await servicer.DeleteEntity(request, mock_grpc_context)
|
|
|
|
assert response.success is True, "delete should succeed"
|
|
mock_entities_repo.delete.assert_called_once_with(entity.id)
|
|
|
|
async def test_delete_aborts_when_entity_belongs_to_different_meeting(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""DeleteEntity aborts when entity belongs to a different meeting."""
|
|
entity_meeting_id = MeetingId(uuid4())
|
|
request_meeting_id = MeetingId(uuid4())
|
|
entity = create_sample_entity(entity_meeting_id, "Test", EntityCategory.PERSON)
|
|
|
|
mock_entities_repo.get.return_value = entity
|
|
|
|
request = noteflow_pb2.DeleteEntityRequest(
|
|
meeting_id=str(request_meeting_id),
|
|
entity_id=str(entity.id),
|
|
)
|
|
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await servicer.DeleteEntity(request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|
|
|
|
async def test_aborts_when_delete_fails(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mock_entities_repo: AsyncMock,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""DeleteEntity aborts when delete operation fails."""
|
|
meeting_id = MeetingId(uuid4())
|
|
entity = create_sample_entity(meeting_id, "Test", EntityCategory.PERSON)
|
|
|
|
mock_entities_repo.get.return_value = entity
|
|
mock_entities_repo.delete.return_value = False
|
|
|
|
request = noteflow_pb2.DeleteEntityRequest(
|
|
meeting_id=str(meeting_id),
|
|
entity_id=str(entity.id),
|
|
)
|
|
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await servicer.DeleteEntity(request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|
|
|
|
@pytest.mark.parametrize(
|
|
("meeting_id", "entity_id"),
|
|
[
|
|
pytest.param("not-a-uuid", str(uuid4()), id="invalid_meeting_id"),
|
|
pytest.param(str(uuid4()), "not-a-uuid", id="invalid_entity_id"),
|
|
],
|
|
)
|
|
async def test_delete_aborts_with_invalid_id_format(
|
|
self,
|
|
servicer: MockServicerHost,
|
|
mock_grpc_context: MagicMock,
|
|
meeting_id: str,
|
|
entity_id: str,
|
|
) -> None:
|
|
request = noteflow_pb2.DeleteEntityRequest(
|
|
meeting_id=meeting_id,
|
|
entity_id=entity_id,
|
|
)
|
|
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await servicer.DeleteEntity(request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|
|
|
|
|
|
class TestDatabaseNotSupported:
|
|
"""Tests for when database/entities are not available."""
|
|
|
|
@pytest.fixture
|
|
def servicer_no_db(
|
|
self,
|
|
mock_entities_repo: AsyncMock,
|
|
mockner_service: MagicMock,
|
|
) -> MockServicerHost:
|
|
"""Create servicer with database not supported."""
|
|
servicer = MockServicerHost(
|
|
mock_entities_repo,
|
|
ner_service=mockner_service,
|
|
)
|
|
provider = MockRepositoryProvider(mock_entities_repo)
|
|
provider.supports_entities = False
|
|
object.__setattr__(servicer, "create_repository_provider", lambda: provider)
|
|
return servicer
|
|
|
|
async def test_update_entity_aborts_without_database(
|
|
self,
|
|
servicer_no_db: MockServicerHost,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""UpdateEntity aborts when database not available."""
|
|
request = noteflow_pb2.UpdateEntityRequest(
|
|
meeting_id=str(uuid4()),
|
|
entity_id=str(uuid4()),
|
|
text="New Text",
|
|
)
|
|
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await servicer_no_db.UpdateEntity(request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|
|
|
|
async def test_delete_entity_aborts_without_database(
|
|
self,
|
|
servicer_no_db: MockServicerHost,
|
|
mock_grpc_context: MagicMock,
|
|
) -> None:
|
|
"""DeleteEntity aborts when database not available."""
|
|
request = noteflow_pb2.DeleteEntityRequest(
|
|
meeting_id=str(uuid4()),
|
|
entity_id=str(uuid4()),
|
|
)
|
|
|
|
with pytest.raises(AssertionError, match="Unreachable"):
|
|
await servicer_no_db.DeleteEntity(request, mock_grpc_context)
|
|
|
|
mock_grpc_context.abort.assert_called_once()
|