299 lines
11 KiB
Python
299 lines
11 KiB
Python
"""Tests for cloud consent gRPC endpoints.
|
|
|
|
Validates the GrantCloudConsent, RevokeCloudConsent, and GetCloudConsentStatus
|
|
RPCs work correctly with the summarization service.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, cast
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from noteflow.application.services.summarization import (
|
|
SummarizationService,
|
|
SummarizationServiceSettings,
|
|
)
|
|
from noteflow.grpc.config.config import ServicesConfig
|
|
from noteflow.grpc.proto import noteflow_pb2
|
|
from noteflow.grpc.service import NoteFlowServicer
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
|
|
class _DummyContext:
|
|
"""Minimal gRPC context for testing."""
|
|
|
|
def __init__(self) -> None:
|
|
self.aborted = False
|
|
self.abort_code: object = None
|
|
self.abort_details: str = ""
|
|
|
|
async def abort(self, code: object, details: str) -> None:
|
|
self.aborted = True
|
|
self.abort_code = code
|
|
self.abort_details = details
|
|
raise AssertionError(f"abort called: {code} - {details}")
|
|
|
|
|
|
def _create_mocksummarization_service(
|
|
*,
|
|
initial_consent: bool = False,
|
|
on_consent_change: Callable[[bool], None] | None = None,
|
|
) -> SummarizationService:
|
|
"""Create a mock summarization service with controllable consent state."""
|
|
settings = SummarizationServiceSettings(summary_consent=initial_consent)
|
|
service = MagicMock(spec=SummarizationService)
|
|
service.settings = settings
|
|
|
|
async def grant(feature: str) -> None:
|
|
settings.cloud_consent_granted = True
|
|
if on_consent_change:
|
|
on_consent_change(True)
|
|
|
|
async def revoke(feature: str) -> None:
|
|
settings.cloud_consent_granted = False
|
|
if on_consent_change:
|
|
on_consent_change(False)
|
|
|
|
service.grant_feature_consent = AsyncMock(side_effect=grant)
|
|
service.revoke_feature_consent = AsyncMock(side_effect=revoke)
|
|
service.cloud_consent_granted = property(lambda _: settings.cloud_consent_granted)
|
|
|
|
# Make cloud_consent_granted work as a property
|
|
type(service).cloud_consent_granted = property(lambda self: self.settings.cloud_consent_granted)
|
|
|
|
return service
|
|
|
|
|
|
class TestGetCloudConsentStatus:
|
|
"""Tests for GetCloudConsentStatus RPC."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_false_when_consent_not_granted(self) -> None:
|
|
"""Status should be false when consent has not been granted."""
|
|
service = _create_mocksummarization_service(initial_consent=False)
|
|
servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))
|
|
|
|
response = await servicer.GetCloudConsentStatus(
|
|
noteflow_pb2.GetCloudConsentStatusRequest(),
|
|
_DummyContext(),
|
|
)
|
|
|
|
assert response.consent_granted is False, "consent should be False when not granted"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_true_when_consent_granted(self) -> None:
|
|
"""Status should be true when consent has been granted."""
|
|
service = _create_mocksummarization_service(initial_consent=True)
|
|
servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))
|
|
|
|
response = await servicer.GetCloudConsentStatus(
|
|
noteflow_pb2.GetCloudConsentStatusRequest(),
|
|
_DummyContext(),
|
|
)
|
|
|
|
assert response.consent_granted is True, "consent should be True when granted"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_false_when_service_unavailable(self) -> None:
|
|
"""Should return false (safe default) when service not configured."""
|
|
servicer = NoteFlowServicer()
|
|
|
|
response = await servicer.GetCloudConsentStatus(
|
|
noteflow_pb2.GetCloudConsentStatusRequest(),
|
|
_DummyContext(),
|
|
)
|
|
|
|
# Safe default: no consent when service unavailable
|
|
assert response.consent_granted is False, "should default to False when service unavailable"
|
|
|
|
|
|
class TestGrantCloudConsent:
|
|
"""Tests for GrantCloudConsent RPC."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grants_consent(self) -> None:
|
|
"""Granting consent should update the service state."""
|
|
service = _create_mocksummarization_service(initial_consent=False)
|
|
servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))
|
|
|
|
response = await servicer.GrantCloudConsent(
|
|
noteflow_pb2.GrantCloudConsentRequest(),
|
|
_DummyContext(),
|
|
)
|
|
|
|
assert isinstance(response, noteflow_pb2.GrantCloudConsentResponse), (
|
|
"GrantCloudConsent should return GrantCloudConsentResponse"
|
|
)
|
|
grant_mock = cast(AsyncMock, service.grant_feature_consent)
|
|
grant_mock.assert_awaited_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grant_is_idempotent(self) -> None:
|
|
"""Granting consent multiple times should not error."""
|
|
service = _create_mocksummarization_service(initial_consent=True)
|
|
servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))
|
|
|
|
# Grant when already granted
|
|
response = await servicer.GrantCloudConsent(
|
|
noteflow_pb2.GrantCloudConsentRequest(),
|
|
_DummyContext(),
|
|
)
|
|
|
|
assert isinstance(response, noteflow_pb2.GrantCloudConsentResponse), (
|
|
"GrantCloudConsent should return GrantCloudConsentResponse even when already granted"
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grant_aborts_when_service_unavailable(self) -> None:
|
|
"""Should abort with FAILED_PRECONDITION when service not configured."""
|
|
servicer = NoteFlowServicer()
|
|
context = _DummyContext()
|
|
|
|
with pytest.raises(AssertionError, match="abort called"):
|
|
await servicer.GrantCloudConsent(
|
|
noteflow_pb2.GrantCloudConsentRequest(),
|
|
context,
|
|
)
|
|
|
|
assert context.aborted, "should abort when service unavailable"
|
|
|
|
|
|
class TestRevokeCloudConsent:
|
|
"""Tests for RevokeCloudConsent RPC."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_revokes_consent(self) -> None:
|
|
"""Revoking consent should update the service state."""
|
|
service = _create_mocksummarization_service(initial_consent=True)
|
|
servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))
|
|
|
|
response = await servicer.RevokeCloudConsent(
|
|
noteflow_pb2.RevokeCloudConsentRequest(),
|
|
_DummyContext(),
|
|
)
|
|
|
|
assert isinstance(response, noteflow_pb2.RevokeCloudConsentResponse), (
|
|
"RevokeCloudConsent should return RevokeCloudConsentResponse"
|
|
)
|
|
revoke_mock = cast(AsyncMock, service.revoke_feature_consent)
|
|
revoke_mock.assert_awaited_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_revoke_is_idempotent(self) -> None:
|
|
"""Revoking consent when not granted should not error."""
|
|
service = _create_mocksummarization_service(initial_consent=False)
|
|
servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))
|
|
|
|
response = await servicer.RevokeCloudConsent(
|
|
noteflow_pb2.RevokeCloudConsentRequest(),
|
|
_DummyContext(),
|
|
)
|
|
|
|
assert isinstance(response, noteflow_pb2.RevokeCloudConsentResponse), (
|
|
"RevokeCloudConsent should return RevokeCloudConsentResponse even when not granted"
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_revoke_aborts_when_service_unavailable(self) -> None:
|
|
"""Should abort with FAILED_PRECONDITION when service not configured."""
|
|
servicer = NoteFlowServicer()
|
|
context = _DummyContext()
|
|
|
|
with pytest.raises(AssertionError, match="abort called"):
|
|
await servicer.RevokeCloudConsent(
|
|
noteflow_pb2.RevokeCloudConsentRequest(),
|
|
context,
|
|
)
|
|
|
|
assert context.aborted, "should abort when service unavailable"
|
|
|
|
|
|
class TestConsentRoundTrip:
|
|
"""Integration tests for consent grant/revoke/status cycle."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grant_then_check_status(self) -> None:
|
|
"""Granting consent should be reflected in status check."""
|
|
service = _create_mocksummarization_service(initial_consent=False)
|
|
servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))
|
|
context = _DummyContext()
|
|
|
|
# Verify initial state
|
|
status_before = await servicer.GetCloudConsentStatus(
|
|
noteflow_pb2.GetCloudConsentStatusRequest(),
|
|
context,
|
|
)
|
|
assert status_before.consent_granted is False, "initial consent should be False"
|
|
|
|
# Grant consent
|
|
await servicer.GrantCloudConsent(
|
|
noteflow_pb2.GrantCloudConsentRequest(),
|
|
_DummyContext(),
|
|
)
|
|
|
|
# Verify new state
|
|
status_after = await servicer.GetCloudConsentStatus(
|
|
noteflow_pb2.GetCloudConsentStatusRequest(),
|
|
_DummyContext(),
|
|
)
|
|
assert status_after.consent_granted is True, "consent should be True after grant"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grant_revoke_cycle(self) -> None:
|
|
"""Full grant/revoke cycle should work correctly."""
|
|
service = _create_mocksummarization_service(initial_consent=False)
|
|
servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))
|
|
|
|
# Grant
|
|
await servicer.GrantCloudConsent(
|
|
noteflow_pb2.GrantCloudConsentRequest(),
|
|
_DummyContext(),
|
|
)
|
|
status = await servicer.GetCloudConsentStatus(
|
|
noteflow_pb2.GetCloudConsentStatusRequest(),
|
|
_DummyContext(),
|
|
)
|
|
assert status.consent_granted is True, "consent should be True after grant"
|
|
|
|
# Revoke
|
|
await servicer.RevokeCloudConsent(
|
|
noteflow_pb2.RevokeCloudConsentRequest(),
|
|
_DummyContext(),
|
|
)
|
|
status = await servicer.GetCloudConsentStatus(
|
|
noteflow_pb2.GetCloudConsentStatusRequest(),
|
|
_DummyContext(),
|
|
)
|
|
assert status.consent_granted is False, "consent should be False after revoke"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_consent_change_callback_invoked(self) -> None:
|
|
"""Consent changes should invoke the on_consent_change callback."""
|
|
callback_values: list[bool] = []
|
|
|
|
def track_changes(granted: bool) -> None:
|
|
callback_values.append(granted)
|
|
|
|
service = _create_mocksummarization_service(
|
|
initial_consent=False,
|
|
on_consent_change=track_changes,
|
|
)
|
|
servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))
|
|
|
|
await servicer.GrantCloudConsent(
|
|
noteflow_pb2.GrantCloudConsentRequest(),
|
|
_DummyContext(),
|
|
)
|
|
await servicer.RevokeCloudConsent(
|
|
noteflow_pb2.RevokeCloudConsentRequest(),
|
|
_DummyContext(),
|
|
)
|
|
|
|
assert callback_values == [True, False], (
|
|
"on_consent_change callback should receive [True, False] for grant then revoke"
|
|
)
|