feat: declare governed external provider state

This commit is contained in:
2026-08-01 17:48:23 +02:00
parent ccdf12162a
commit 66a6df4f05
5 changed files with 761 additions and 0 deletions
+6
View File
@@ -1,5 +1,11 @@
# GovOPlaN Calendar Codex Guide # GovOPlaN Calendar Codex Guide
## Documentation Contract
- Treat documentation as part of every behavior change. Update this module's manifest-driven `DocumentationTopic` contributions for affected user and administrator behavior.
- Keep feature content here; `govoplan-docs` projects it without importing Calendar internals.
- Maintain a static user/admin baseline and run `/mnt/DATA/git/govoplan/tools/checks/check-manifest-shapes.py` after behavior or manifest changes.
## Scope ## Scope
This repository owns the `calendar` module: calendar collections, VEVENT storage, iCalendar import/export, availability primitives, backend module manifest, and `@govoplan/calendar-webui`. This repository owns the `calendar` module: calendar collections, VEVENT storage, iCalendar import/export, availability primitives, backend module manifest, and `@govoplan/calendar-webui`.
+16
View File
@@ -81,6 +81,22 @@ retirement before queued work is changed; a later retry tolerates a provider
secret already removed by an earlier attempt whose database transaction rolled secret already removed by an earlier attempt whose database transaction rolled
back. Provider errors and audit details never contain credential values or back. Provider errors and audit details never contain credential values or
secret references. secret references.
### Runtime provider state
Calendar registers tenant-aware runtime state for
`calendar.caldav_sync`. Each active CalDAV source is projected by a stable
`calendar:sync-source:<id>` reference with its effective source-authority mode,
enabled state, health, freshness, unresolved-conflict state, recovery readiness,
last successful synchronization, and bounded outbox counts. Collection URLs,
usernames, credential references, remote resource paths, and error text are not
included.
Docs uses the projection to explain configured availability, Ops aggregates it
for operator inspection, and configuration-package preflight can require one
exact source binding. A source with dead or conflicting desired-state work
requires recovery attention; a never-synchronized source remains unknown rather
than being reported healthy.
The current singular ownership model permits one active sync source per The current singular ownership model permits one active sync source per
calendar; multi-source fan-in will require per-event source routing. Celery beat calendar; multi-source fan-in will require per-event source routing. Celery beat
triggers recovery every minute, while root-transaction after-commit dispatch triggers recovery every minute, while root-transaction after-commit dispatch
+286
View File
@@ -16,6 +16,7 @@ from govoplan_core.core.calendar import (
) )
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
from govoplan_core.core.modules import ( from govoplan_core.core.modules import (
DocumentationTopic,
FrontendModule, FrontendModule,
FrontendRoute, FrontendRoute,
MigrationSpec, MigrationSpec,
@@ -26,10 +27,225 @@ from govoplan_core.core.modules import (
PermissionDefinition, PermissionDefinition,
RoleTemplate, RoleTemplate,
) )
from govoplan_core.core.provider_governance import (
ExternalProviderDeclaration,
ExternalProviderStateProviderRegistration,
ModuleArchitectureDeclaration,
ModuleArchitectureDocumentation,
ModuleMaturityEvidence,
ProviderBehaviorDeclaration,
ProviderObjectDeclaration,
)
from govoplan_core.core.views import ViewSurface from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base from govoplan_core.db.base import Base
CALDAV_PROVIDER_ID = "calendar.caldav_sync"
ICS_PROVIDER_ID = "calendar.ics_subscription"
GRAPH_PROVIDER_ID = "calendar.microsoft_graph"
EWS_PROVIDER_ID = "calendar.exchange_ews"
CALENDAR_ARCHITECTURE = ModuleArchitectureDeclaration(
layer="communication_participation",
kind="domain",
maturity="vertical_slice",
evidence=(
ModuleMaturityEvidence(
kind="test",
reference="tests/test_caldav.py",
summary="Exercises CalDAV discovery, pull, push, deletion, and reconciliation behavior.",
),
ModuleMaturityEvidence(
kind="test",
reference="tests/test_outbox.py",
summary="Exercises durable calendar effects, retries, terminal states, and cleanup.",
),
ModuleMaturityEvidence(
kind="documentation",
reference="docs/CALENDAR_INTEGRATION_CONCEPT.md",
summary="Documents Calendar ownership and optional integration boundaries.",
),
),
known_limits=(
"Provider target-environment and recovery-drill evidence is not yet packaged as a reference package.",
"Microsoft Graph and EWS adapters do not yet have the same write/reconciliation coverage as CalDAV.",
),
supported_authority_modes=(
"native_authoritative",
"external_authoritative",
"external_mirror",
"governed_sync",
"linked_reference",
),
owned_concepts=(
"calendar collections",
"VEVENT lifecycle",
"recurrence and availability",
"calendar synchronization state",
),
non_owned_concepts=(
"meeting polls",
"mail delivery",
"task lifecycle",
"external identity and credential secrets",
),
target_tested_providers=(CALDAV_PROVIDER_ID,),
documentation=ModuleArchitectureDocumentation(
migration=("src/govoplan_calendar/backend/migrations/versions",),
upgrade=("docs/CALENDAR_INTEGRATION_CONCEPT.md",),
recovery=("docs/CALENDAR_INTEGRATION_CONCEPT.md",),
security=("tests/test_caldav_security.py", "tests/test_http_security.py"),
operations=("docs/CALENDAR_INTEGRATION_CONCEPT.md",),
),
)
def _read_only_calendar_provider(
*,
provider_id: str,
label: str,
source_name: str,
) -> ExternalProviderDeclaration:
return ExternalProviderDeclaration(
id=provider_id,
module_id="calendar",
label=label,
maturity="read",
operations=("read", "preview"),
objects=(
ProviderObjectDeclaration(
object_type="calendar_collection",
field_groups=("identity", "display", "sync_state"),
authority_modes=("external_authoritative", "external_mirror"),
default_authority_mode="external_mirror",
),
ProviderObjectDeclaration(
object_type="calendar_event",
field_groups=(
"identity",
"schedule",
"recurrence",
"participants",
"content",
),
authority_modes=("external_authoritative", "external_mirror"),
default_authority_mode="external_mirror",
),
),
behavior=ProviderBehaviorDeclaration(
revision_tokens=f"{source_name} object identity, remote revision, and content fingerprint are retained.",
concurrency="The source is inbound-only; refresh compares remote identity and revision before replacing the derived projection.",
freshness="Last attempt, last success, and source status are retained.",
health="Authentication, transport, parsing, and source failures are recorded without exposing credentials.",
max_read_items=5000,
evidence=f"Imported events retain the {source_name} source, remote identity, revision, and normalized content fingerprint.",
audit_event_types=("calendar.sync.requested", "calendar.sync.completed"),
correction="A later successful refresh creates the corrected local projection while source authority remains external.",
reconciliation="Re-read the bounded source and compare remote identity, revision, and normalized event content.",
outage="The last local projection remains available with stale or unknown freshness.",
classifications=("internal", "confidential", "restricted"),
purposes=("calendar subscription", "availability", "meeting coordination"),
retention="Calendar projection, sync evidence, and credential retention are independent policies.",
secret_handling="Authentication material remains in Calendar credential records or deployment-owned secret references.",
),
documentation_topic_ids=("calendar.external-sources-and-sync",),
)
CALENDAR_EXTERNAL_PROVIDERS = (
ExternalProviderDeclaration(
id=CALDAV_PROVIDER_ID,
module_id="calendar",
label="CalDAV calendar synchronization",
maturity="synchronize",
operations=(
"discover",
"read",
"write",
"delete",
"synchronize",
"preview",
),
objects=(
ProviderObjectDeclaration(
object_type="calendar_collection",
field_groups=("identity", "display", "sync_state"),
authority_modes=(
"native_authoritative",
"external_authoritative",
"external_mirror",
"governed_sync",
),
default_authority_mode="governed_sync",
),
ProviderObjectDeclaration(
object_type="calendar_event",
field_groups=(
"identity",
"schedule",
"recurrence",
"participants",
"content",
),
authority_modes=(
"native_authoritative",
"external_authoritative",
"external_mirror",
"governed_sync",
),
default_authority_mode="governed_sync",
),
),
behavior=ProviderBehaviorDeclaration(
revision_tokens="CalDAV sync tokens and per-resource ETags are retained.",
concurrency="Conditional requests reject stale ETags; conflicts remain explicit.",
freshness="Last attempt, last success, sync token, and pending outbox work are visible.",
health="Discovery, authentication, transport, collection, and reconciliation failures are recorded.",
max_read_items=5000,
idempotency="Stable VEVENT UID, source id, and durable operation keys suppress duplicate effects.",
retry="Only classified transient failures receive bounded backoff retries.",
timeout_seconds=30,
conflicts="Stale revisions and concurrent remote changes enter conflict/reconciliation state.",
outcome_unknown="A timed-out remote write is outcome-unknown and is not blindly repeated.",
outcome_unknown_supported=True,
evidence="Operation id, request intent, ETag/sync token, response classification, and reconciliation result are retained.",
audit_event_types=(
"calendar.sync.requested",
"calendar.sync.completed",
"calendar.sync.conflict",
"calendar.sync.reconciled",
),
correction="A later conditional update or tombstone corrects external state after reconciliation.",
rollback="Remote effects are not treated as transactionally rollback-safe.",
compensation="A compensating event update/delete may be queued after the remote state is known.",
reconciliation="Read the resource by href/UID, compare ETag and content, then classify applied, retryable, conflict, or manual.",
outage="Local calendars remain usable with stale markers; pending writes stay durable and bounded.",
classifications=("internal", "confidential", "restricted"),
purposes=("calendar collaboration", "availability", "meeting coordination"),
retention="Calendar event, outbox terminal-state, audit, and credential retention are independent policies.",
secret_handling="Authentication material is stored through Calendar credential records or external secret references and is never emitted in provider metadata.",
),
capability_names=(CAPABILITY_CALENDAR_OUTBOX,),
interface_names=("calendar.outbox",),
documentation_topic_ids=("calendar.external-sources-and-sync",),
),
_read_only_calendar_provider(
provider_id=ICS_PROVIDER_ID,
label="ICS and webcal subscription",
source_name="ICS/webcal",
),
_read_only_calendar_provider(
provider_id=GRAPH_PROVIDER_ID,
label="Microsoft Graph calendar projection",
source_name="Microsoft Graph",
),
_read_only_calendar_provider(
provider_id=EWS_PROVIDER_ID,
label="Exchange Web Services calendar projection",
source_name="Exchange Web Services",
),
)
_calendar_table_retirement_provider = drop_table_retirement_provider( _calendar_table_retirement_provider = drop_table_retirement_provider(
calendar_models.CalendarCollection, calendar_models.CalendarCollection,
calendar_models.CalendarEvent, calendar_models.CalendarEvent,
@@ -185,6 +401,30 @@ def _calendar_outbox_provider(context: ModuleContext) -> object:
) )
def _caldav_provider_states(context):
from govoplan_calendar.backend.provider_state import caldav_provider_states
return caldav_provider_states(context)
def _ics_provider_states(context):
from govoplan_calendar.backend.provider_state import ics_provider_states
return ics_provider_states(context)
def _graph_provider_states(context):
from govoplan_calendar.backend.provider_state import graph_provider_states
return graph_provider_states(context)
def _ews_provider_states(context):
from govoplan_calendar.backend.provider_state import ews_provider_states
return ews_provider_states(context)
manifest = ModuleManifest( manifest = ModuleManifest(
id="calendar", id="calendar",
name="Calendar", name="Calendar",
@@ -200,6 +440,52 @@ manifest = ModuleManifest(
route_factory=_calendar_router, route_factory=_calendar_router,
role_templates=ROLE_TEMPLATES, role_templates=ROLE_TEMPLATES,
tenant_summary_providers=(_tenant_summary,), tenant_summary_providers=(_tenant_summary,),
architecture=CALENDAR_ARCHITECTURE,
external_providers=CALENDAR_EXTERNAL_PROVIDERS,
external_provider_state_providers=(
ExternalProviderStateProviderRegistration(
module_id="calendar",
provider_id=CALDAV_PROVIDER_ID,
provider=_caldav_provider_states,
),
ExternalProviderStateProviderRegistration(
module_id="calendar",
provider_id=ICS_PROVIDER_ID,
provider=_ics_provider_states,
),
ExternalProviderStateProviderRegistration(
module_id="calendar",
provider_id=GRAPH_PROVIDER_ID,
provider=_graph_provider_states,
),
ExternalProviderStateProviderRegistration(
module_id="calendar",
provider_id=EWS_PROVIDER_ID,
provider=_ews_provider_states,
),
),
documentation=(
DocumentationTopic(
id="calendar.manage-calendars-and-events",
title="Use calendars and events",
summary="Create calendar collections and work with all-day or timed events in continuous, month, week, workweek, and day views.",
body="Calendar remembers the selected view and preferences. Events can be created, edited, moved, resized, repeated, imported, exported, or deleted when the current account has the corresponding permission. All-day events use dates rather than local clock times; timed events retain their timezone-aware start and end values.",
documentation_types=("user",),
audience=("user", "calendar_manager"),
related_modules=("scheduling", "notifications"),
metadata={"kind": "reference"},
),
DocumentationTopic(
id="calendar.external-sources-and-sync",
title="Connect and synchronize external calendars",
summary="Calendar supports local collections, two-way CalDAV, and read-only ICS/webcal, Microsoft Graph, and Exchange Web Services sources.",
body="Each external source keeps its URL, synchronization direction, status, and credential reference with the Calendar collection. Manual or scheduled synchronization records bounded outcomes. CalDAV writes use conditional requests and durable outbox state; conflicts and unknown outcomes require synchronization or explicit reconciliation instead of blind repetition. Removing an external source removes the connection, while deleting a local calendar deletes its owned events after confirmation or transfer.",
documentation_types=("admin", "user"),
audience=("user", "calendar_manager", "operator"),
related_modules=("connectors", "audit", "ops"),
metadata={"kind": "reference"},
),
),
capability_factories={ capability_factories={
CAPABILITY_CALENDAR_OUTBOX: _calendar_outbox_provider, CAPABILITY_CALENDAR_OUTBOX: _calendar_outbox_provider,
CAPABILITY_CALENDAR_SCHEDULING: _calendar_scheduling_provider, CAPABILITY_CALENDAR_SCHEDULING: _calendar_scheduling_provider,
@@ -0,0 +1,279 @@
from __future__ import annotations
from collections import defaultdict
from datetime import UTC, datetime, timedelta
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from govoplan_calendar.backend.db.models import (
CalendarOutboxOperation,
CalendarSyncSource,
)
from govoplan_core.core.provider_governance import (
ExternalProviderRuntimeState,
ExternalProviderStateContext,
)
CALDAV_PROVIDER_ID = "calendar.caldav_sync"
ICS_PROVIDER_ID = "calendar.ics_subscription"
GRAPH_PROVIDER_ID = "calendar.microsoft_graph"
EWS_PROVIDER_ID = "calendar.exchange_ews"
_HEALTHY_STATUSES = frozenset({"ok", "outbound_ok"})
_ERROR_STATUSES = frozenset({"error", "outbound_error"})
_WARNING_STATUSES = frozenset(
{"outbound_retry", "outbound_cancelled", "outbound_discarded"}
)
_ACTIVE_OUTBOX_STATUSES = frozenset({"pending", "retry", "in_progress"})
def caldav_provider_states(
context: ExternalProviderStateContext,
) -> tuple[ExternalProviderRuntimeState, ...]:
return _provider_states(
context,
provider_id=CALDAV_PROVIDER_ID,
source_kinds=("caldav",),
include_outbox=True,
)
def ics_provider_states(
context: ExternalProviderStateContext,
) -> tuple[ExternalProviderRuntimeState, ...]:
return _provider_states(
context,
provider_id=ICS_PROVIDER_ID,
source_kinds=("ics", "webcal"),
)
def graph_provider_states(
context: ExternalProviderStateContext,
) -> tuple[ExternalProviderRuntimeState, ...]:
return _provider_states(
context,
provider_id=GRAPH_PROVIDER_ID,
source_kinds=("graph",),
)
def ews_provider_states(
context: ExternalProviderStateContext,
) -> tuple[ExternalProviderRuntimeState, ...]:
return _provider_states(
context,
provider_id=EWS_PROVIDER_ID,
source_kinds=("ews",),
)
def _provider_states(
context: ExternalProviderStateContext,
*,
provider_id: str,
source_kinds: tuple[str, ...],
include_outbox: bool = False,
) -> tuple[ExternalProviderRuntimeState, ...]:
if not isinstance(context.session, Session):
raise RuntimeError("Calendar provider state requires a database session.")
statement = select(CalendarSyncSource).where(
CalendarSyncSource.source_kind.in_(source_kinds),
CalendarSyncSource.deleted_at.is_(None),
)
if context.tenant_id is not None:
statement = statement.where(
CalendarSyncSource.tenant_id == context.tenant_id
)
sources = tuple(
context.session.scalars(
statement.order_by(
CalendarSyncSource.tenant_id,
CalendarSyncSource.id,
).limit(context.max_items + 1)
)
)
if not sources:
return ()
counts = (
_outbox_counts(context.session, tuple(item.id for item in sources))
if include_outbox
else {}
)
observed_at = datetime.now(UTC)
return tuple(
_source_state(
source,
provider_id=provider_id,
observed_at=observed_at,
outbox_counts=counts.get(source.id, {}),
)
for source in sources
)
def _outbox_counts(
session: Session,
source_ids: tuple[str, ...],
) -> dict[str, dict[str, int]]:
result: dict[str, dict[str, int]] = defaultdict(dict)
if not source_ids:
return result
rows = session.execute(
select(
CalendarOutboxOperation.source_id,
CalendarOutboxOperation.status,
func.count(CalendarOutboxOperation.id),
)
.where(CalendarOutboxOperation.source_id.in_(source_ids))
.group_by(
CalendarOutboxOperation.source_id,
CalendarOutboxOperation.status,
)
)
for source_id, status, count in rows:
result[str(source_id)][str(status)] = int(count)
return result
def _source_state(
source: CalendarSyncSource,
*,
provider_id: str,
observed_at: datetime,
outbox_counts: dict[str, int],
) -> ExternalProviderRuntimeState:
active = bool(source.sync_enabled)
conflict_count = int(outbox_counts.get("conflict", 0))
dead_count = int(outbox_counts.get("dead", 0))
pending_count = sum(
int(outbox_counts.get(status, 0))
for status in _ACTIVE_OUTBOX_STATUSES
)
health = _health_state(
active=active,
last_status=source.last_status,
conflict_count=conflict_count,
dead_count=dead_count,
pending_count=pending_count,
)
conflict = (
"blocked"
if dead_count
else "pending"
if conflict_count
else "clear"
)
freshness = _freshness_state(source, observed_at=observed_at)
recovery = (
"not_applicable"
if not active
else "attention"
if conflict_count or dead_count or health == "error"
else "ready"
)
return ExternalProviderRuntimeState(
provider_id=provider_id,
binding_ref=f"calendar:sync-source:{source.id}",
authority_mode=(
"governed_sync"
if source.source_kind == "caldav" and source.sync_direction == "two_way"
else "external_mirror"
),
observed_at=observed_at,
configured=True,
active=active,
health=health,
freshness=freshness,
conflict=conflict,
recovery=recovery,
last_success_at=_aware(source.last_synced_at),
detail=_state_detail(
active=active,
health=health,
freshness=freshness,
conflict=conflict,
),
metrics={
"pending_operations": pending_count,
"conflict_operations": conflict_count,
"dead_operations": dead_count,
"sync_interval_seconds": source.sync_interval_seconds,
},
)
def _health_state(
*,
active: bool,
last_status: str | None,
conflict_count: int,
dead_count: int,
pending_count: int,
) -> str:
if not active:
return "inactive"
if dead_count or last_status in _ERROR_STATUSES:
return "error"
if conflict_count or last_status in _WARNING_STATUSES:
return "warning"
if last_status in _HEALTHY_STATUSES:
return "healthy"
if pending_count:
return "warning"
return "unknown"
def _freshness_state(
source: CalendarSyncSource,
*,
observed_at: datetime,
) -> str:
if not source.sync_enabled:
return "not_applicable"
last_synced_at = _aware(source.last_synced_at)
if last_synced_at is None:
return "unknown"
interval = max(int(source.sync_interval_seconds or 0), 60)
grace = timedelta(seconds=max(interval * 2, 1_800))
return "current" if observed_at - last_synced_at <= grace else "stale"
def _state_detail(
*,
active: bool,
health: str,
freshness: str,
conflict: str,
) -> str:
if not active:
return "Synchronization is configured but disabled."
if conflict != "clear":
return "Synchronization has unresolved external outcomes."
if health == "error":
return "The latest synchronization attempt failed."
if freshness == "stale":
return "The last successful synchronization is stale."
if health == "healthy":
return "Synchronization is healthy."
return "Synchronization has not produced a conclusive health observation."
def _aware(value: datetime | None) -> datetime | None:
if value is None:
return None
return value.replace(tzinfo=UTC) if value.tzinfo is None else value
__all__ = [
"CALDAV_PROVIDER_ID",
"EWS_PROVIDER_ID",
"GRAPH_PROVIDER_ID",
"ICS_PROVIDER_ID",
"caldav_provider_states",
"ews_provider_states",
"graph_provider_states",
"ics_provider_states",
]
+174
View File
@@ -0,0 +1,174 @@
from __future__ import annotations
from datetime import UTC, datetime
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from govoplan_access.backend.db import models as access_models # noqa: F401
from govoplan_calendar.backend.db.models import CalendarOutboxOperation, CalendarSyncSource
from govoplan_calendar.backend.manifest import (
CALDAV_PROVIDER_ID,
EWS_PROVIDER_ID,
GRAPH_PROVIDER_ID,
ICS_PROVIDER_ID,
manifest,
)
from govoplan_calendar.backend.provider_state import (
caldav_provider_states,
ews_provider_states,
graph_provider_states,
ics_provider_states,
)
from govoplan_calendar.backend.schemas import (
CalendarCalDavSourceCreateRequest,
CalendarCollectionCreateRequest,
)
from govoplan_calendar.backend.service import create_caldav_source, create_calendar
from govoplan_core.core.provider_governance import ExternalProviderStateContext
from govoplan_core.db.base import Base
from govoplan_core.tenancy.scope import create_scope_tables
from govoplan_tenancy.backend.db.models import Tenant
class CalendarProviderStateTests(unittest.TestCase):
def setUp(self) -> None:
self.engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
create_scope_tables(self.engine)
Base.metadata.create_all(self.engine)
self.Session = sessionmaker(bind=self.engine, expire_on_commit=False)
self.session = self.Session()
self.session.add(Tenant(id="tenant-1", slug="tenant-1", name="Tenant"))
self.calendar = create_calendar(
self.session,
tenant_id="tenant-1",
user_id=None,
payload=CalendarCollectionCreateRequest(name="Remote"),
)
self.source = create_caldav_source(
self.session,
tenant_id="tenant-1",
user_id=None,
payload=CalendarCalDavSourceCreateRequest(
calendar_id=self.calendar.id,
collection_url="https://dav.example.test/cal",
),
)
self.session.commit()
def tearDown(self) -> None:
self.session.close()
self.engine.dispose()
def test_state_projects_health_freshness_and_unresolved_outcomes(self) -> None:
self.source.last_status = "ok"
self.source.last_synced_at = datetime.now(UTC)
self.session.flush()
healthy = caldav_provider_states(
ExternalProviderStateContext(
session=self.session,
tenant_id="tenant-1",
)
)[0]
self.assertEqual("healthy", healthy.health)
self.assertEqual("current", healthy.freshness)
self.assertEqual("clear", healthy.conflict)
self.assertEqual("ready", healthy.recovery)
self.assertEqual(
f"calendar:sync-source:{self.source.id}",
healthy.binding_ref,
)
self.assertNotIn("dav.example.test", str(healthy.to_dict()))
self.session.add(
CalendarOutboxOperation(
tenant_id="tenant-1",
source_id=self.source.id,
operation_kind="put",
resource_href="event.ics",
idempotency_key="a" * 64,
status="conflict",
)
)
self.session.flush()
conflicted = caldav_provider_states(
ExternalProviderStateContext(
session=self.session,
tenant_id="tenant-1",
)
)[0]
self.assertEqual("warning", conflicted.health)
self.assertEqual("pending", conflicted.conflict)
self.assertEqual("attention", conflicted.recovery)
def test_state_is_tenant_bounded_and_manifest_registered(self) -> None:
self.assertEqual(
(),
caldav_provider_states(
ExternalProviderStateContext(
session=self.session,
tenant_id="tenant-2",
)
),
)
self.assertEqual(
{CALDAV_PROVIDER_ID, ICS_PROVIDER_ID, GRAPH_PROVIDER_ID, EWS_PROVIDER_ID},
{item.id for item in manifest.external_providers},
)
self.assertEqual(
{CALDAV_PROVIDER_ID, ICS_PROVIDER_ID, GRAPH_PROVIDER_ID, EWS_PROVIDER_ID},
{
item.provider_id
for item in manifest.external_provider_state_providers
},
)
def test_read_only_sources_have_distinct_secret_free_provider_state(self) -> None:
now = datetime.now(UTC)
sources = (
("ics", "https://feeds.example.test/team.ics", "none"),
("graph", "https://graph.microsoft.com/v1.0/me/calendar/events", "bearer"),
("ews", "https://exchange.example.test/EWS/Exchange.asmx", "basic"),
)
for source_kind, collection_url, auth_type in sources:
self.session.add(
CalendarSyncSource(
tenant_id="tenant-1",
calendar_id=self.calendar.id,
source_kind=source_kind,
collection_url=collection_url,
auth_type=auth_type,
sync_enabled=True,
sync_direction="read_only",
last_status="ok",
last_synced_at=now,
)
)
self.session.flush()
states = (
ics_provider_states(
ExternalProviderStateContext(session=self.session, tenant_id="tenant-1")
)[0],
graph_provider_states(
ExternalProviderStateContext(session=self.session, tenant_id="tenant-1")
)[0],
ews_provider_states(
ExternalProviderStateContext(session=self.session, tenant_id="tenant-1")
)[0],
)
self.assertTrue(all(item.authority_mode == "external_mirror" for item in states))
self.assertTrue(all(item.health == "healthy" for item in states))
rendered = str([item.to_dict() for item in states])
self.assertNotIn("feeds.example.test", rendered)
self.assertNotIn("exchange.example.test", rendered)
if __name__ == "__main__":
unittest.main()