Files
noteflow/tests/grpc/test_cloud_consent.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

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