from __future__ import annotations import unittest from unittest.mock import patch from defusedxml import ElementTree as SafeET from fastapi import HTTPException from govoplan_connectors.backend.feeds import ( FEED_PRIVATE_PUBLISH_SCOPE, FEED_PUBLISH_SCOPE, ConnectorFeedProvider, feed_rows, ) from govoplan_connectors.backend.router import api_render_feed from govoplan_connectors.backend.schemas import FeedRenderPayload from govoplan_core.auth import ApiPrincipal from govoplan_core.core.access import PrincipalRef from govoplan_core.core.feeds import ( FeedCapabilityError, FeedEntry, FeedRenderRequest, ) from govoplan_core.security.http_fetch import HttpFetchResponse RSS = b""" Decisionshttps://example.test/ Published decisions decision-1Decision one https://example.test/1 Fri, 31 Jul 2026 10:00:00 GMT planning """ ATOM = b""" https://example.test/feedUpdates 2026-07-31T10:00:00Z update-1Update one 2026-07-31T10:00:00Z """ class ConnectorFeedProviderTests(unittest.TestCase): def setUp(self) -> None: self.provider = ConnectorFeedProvider() def test_rss_and_atom_are_normalized_to_tabular_entries(self) -> None: rss = self.provider.parse(RSS, source_url="https://example.test/rss") atom = self.provider.parse(ATOM, source_url="https://example.test/atom") self.assertEqual("rss", rss.format) self.assertEqual("decision-1", rss.entries[0].id) self.assertEqual("atom", atom.format) self.assertEqual("https://example.test/update-1", atom.entries[0].url) self.assertEqual("decision-1", feed_rows(rss)[0]["id"]) self.assertEqual(64, len(rss.sha256)) def test_fetch_records_transport_freshness_and_provenance(self) -> None: with patch( "govoplan_connectors.backend.feeds.fetch_http", return_value=HttpFetchResponse( status=200, headers={ "Content-Type": "application/rss+xml", "Cache-Control": "public, max-age=600", "ETag": '"feed-1"', }, body=RSS, ), ): document = self.provider.fetch("https://example.test/rss") self.assertEqual('"feed-1"', document.etag) self.assertIsNotNone(document.acquired_at) self.assertEqual(600, int((document.fresh_until - document.acquired_at).total_seconds())) self.assertEqual(len(RSS), document.metadata["byte_count"]) def test_render_filters_entries_by_explicit_visibility(self) -> None: request = FeedRenderRequest( format="atom", title="Public updates", feed_url="https://example.test/feed.atom", home_url="https://example.test/", entries=( FeedEntry(id="public-1", title="Public", visibility="public"), FeedEntry(id="tenant-1", title="Tenant", visibility="tenant"), ), allowed_visibilities=frozenset({"public"}), ) result = self.provider.render(request) root = SafeET.fromstring(result.body) self.assertEqual(1, result.included_entries) self.assertEqual(1, result.excluded_entries) self.assertIn(b"Public", result.body) self.assertNotIn(b"Tenant", result.body) self.assertTrue(root.tag.endswith("feed")) def test_unsafe_xml_is_rejected(self) -> None: payload = b']>&y;' with self.assertRaisesRegex(FeedCapabilityError, "not safe or valid"): self.provider.parse(payload, source_url="https://example.test/rss") def test_render_api_derives_visibility_from_audience_and_permissions(self) -> None: entries = [ { "id": visibility, "title": visibility.title(), "visibility": visibility, "source_kind": source_kind, "source_module": source_module, "source_ref": f"{source_kind}:{visibility}", "source_revision": "7", } for visibility, source_kind, source_module in ( ("public", "publication", "docs"), ("tenant", "case", "cases"), ("private", "report", "reporting"), ) ] public_payload = FeedRenderPayload( format="rss", title="Selected GovOPlaN updates", feed_url="https://example.test/feed.xml", home_url="https://example.test/", audience="public", entries=entries, ) public = api_render_feed( public_payload, principal=_principal(FEED_PUBLISH_SCOPE), ) self.assertEqual("public", public.headers["x-govoplan-feed-audience"]) self.assertEqual("1", public.headers["x-govoplan-feed-included"]) self.assertEqual("2", public.headers["x-govoplan-feed-excluded"]) self.assertIn(b"Public", public.body) self.assertNotIn(b"Tenant", public.body) restricted_payload = public_payload.model_copy(update={"audience": "private"}) with self.assertRaises(HTTPException) as denied: api_render_feed( restricted_payload, principal=_principal(FEED_PUBLISH_SCOPE), ) self.assertEqual(403, denied.exception.status_code) restricted = api_render_feed( restricted_payload, principal=_principal( FEED_PUBLISH_SCOPE, FEED_PRIVATE_PUBLISH_SCOPE, ), ) self.assertEqual("3", restricted.headers["x-govoplan-feed-included"]) self.assertIn(b"Private", restricted.body) def _principal(*scopes: str) -> ApiPrincipal: return ApiPrincipal( principal=PrincipalRef( account_id="account-1", membership_id="membership-1", tenant_id="tenant-1", scopes=frozenset(scopes), ), account=object(), user=object(), ) if __name__ == "__main__": unittest.main()