relay/app/feeds/test_discover_relay_nodes.py
Andros Fenollosa c446ae514d Exclude bridge virtual feeds from feed listing, discovery and stats
Bridges help users connect to external content (RSS, ActivityPub) but
are not real Org Social accounts. They no longer appear in /feeds/,
cannot be registered via POST, are skipped by both feed discovery
tasks, and do not count in /stats/. A data migration removes bridge
URLs already registered as feeds.
2026-07-20 09:33:28 +02:00

231 lines
9 KiB
Python

from django.test import TestCase
from django.conf import settings
from unittest.mock import patch, Mock
from app.feeds.models import Feed
from app.feeds.tasks import discover_feeds_from_relay_nodes
class DiscoverRelayNodesTest(TestCase):
"""Test cases for discover_feeds_from_relay_nodes task using Given/When/Then structure."""
def setUp(self):
"""Set up test fixtures."""
# Clear any existing feeds
Feed.objects.all().delete()
def test_discovers_feeds_from_real_relay_list(self):
"""Test that feeds are discovered from the actual relay list file."""
# Given: The real relay list file exists (data/relay-list.txt)
# And we mock the HTTP responses from relays
# Mock relay response
mock_relay_response = Mock()
mock_relay_response.status_code = 200
mock_relay_response.json.return_value = {
"type": "Success",
"data": [
"https://test-feed1.com/social.org",
"https://test-feed2.com/social.org",
],
}
mock_relay_response.raise_for_status = Mock()
# Mock feed validation responses
mock_feed_response = Mock()
mock_feed_response.status_code = 200
mock_feed_response.text = """#+TITLE: Test Feed
#+NICK: testuser
#+DESCRIPTION: Test description
* Posts
"""
mock_feed_response.content = mock_feed_response.text.encode("utf-8")
mock_feed_response.history = []
def mock_requests_get(url, timeout=None):
if "/feeds" in url:
return mock_relay_response
# Feed validation requests
mock_feed_response.url = url
mock_feed_response.raise_for_status = Mock()
return mock_feed_response
# When: We run the discovery task
with patch("requests.get", side_effect=mock_requests_get):
discover_feeds_from_relay_nodes()
# Then: Feeds should be discovered (the real relay list has relays)
# We can't predict exact count since it depends on real file content
# So we just verify the function runs without errors
self.assertGreaterEqual(Feed.objects.count(), 0)
def test_handles_relay_connection_failure(self):
"""Test that the task handles relay connection failures gracefully."""
# Given: The real relay list file exists
# And relay connections will fail
# Mock a connection error
with patch("requests.get", side_effect=Exception("Connection failed")):
# When: We run the discovery task
initial_count = Feed.objects.count()
discover_feeds_from_relay_nodes()
# Then: No new feeds should be created and no exception raised
self.assertEqual(Feed.objects.count(), initial_count)
def test_skips_existing_feeds(self):
"""Test that the task skips feeds that already exist."""
# Given: A feed already exists
Feed.objects.create(url="https://existing-feed.com/social.org")
initial_count = Feed.objects.count()
# Mock relay response with the existing feed
mock_relay_response = Mock()
mock_relay_response.status_code = 200
mock_relay_response.json.return_value = {
"type": "Success",
"data": [
"https://existing-feed.com/social.org", # Already exists
],
}
mock_relay_response.raise_for_status = Mock()
def mock_requests_get(url, timeout=None):
if "/feeds" in url:
return mock_relay_response
# Should not validate existing feed
if "existing-feed" in url:
self.fail("Should not validate existing feed")
return Mock()
# When: We run the discovery task
with patch("requests.get", side_effect=mock_requests_get):
discover_feeds_from_relay_nodes()
# Then: No new feeds should be added
self.assertEqual(Feed.objects.count(), initial_count)
def test_handles_invalid_feed_during_validation(self):
"""Test that the task skips invalid feeds during validation."""
# Given: The real relay list file exists
# Mock relay response with valid and invalid feeds
mock_relay_response = Mock()
mock_relay_response.status_code = 200
mock_relay_response.json.return_value = {
"type": "Success",
"data": [
"https://valid-feed.com/social.org",
"https://invalid-feed.com/social.org",
],
}
mock_relay_response.raise_for_status = Mock()
# Mock feed validation responses
def mock_requests_get(url, timeout=None):
if "/feeds" in url:
return mock_relay_response
# Valid feed response
if "valid-feed" in url:
mock_response = Mock()
mock_response.status_code = 200
mock_response.text = """#+TITLE: Valid Feed
#+NICK: validuser
#+DESCRIPTION: Valid description
* Posts
"""
mock_response.content = mock_response.text.encode("utf-8")
mock_response.url = url
mock_response.history = []
mock_response.raise_for_status = Mock()
return mock_response
# Invalid feed response (missing required fields)
if "invalid-feed" in url:
mock_response = Mock()
mock_response.status_code = 200
mock_response.text = "Invalid content without Org Social headers"
mock_response.content = mock_response.text.encode("utf-8")
mock_response.url = url
mock_response.history = []
mock_response.raise_for_status = Mock()
return mock_response
# Default response for any other URL
mock_response = Mock()
mock_response.status_code = 404
mock_response.raise_for_status = Mock(side_effect=Exception("404"))
return mock_response
# When: We run the discovery task
initial_count = Feed.objects.count()
with patch("requests.get", side_effect=mock_requests_get):
discover_feeds_from_relay_nodes()
# Then: Only the valid feed should be added (or none if validation failed for both)
# We verify that invalid feed was not added
self.assertFalse(
Feed.objects.filter(url="https://invalid-feed.com/social.org").exists()
)
# And if valid feed was processed, it should exist
# (we're more lenient here as the test DB might interfere)
if Feed.objects.count() > initial_count:
self.assertTrue(
Feed.objects.filter(url="https://valid-feed.com/social.org").exists()
)
def test_filters_out_own_domain(self):
"""Test that the task filters out its own domain from relay list."""
# Given: The real relay list file exists
# We verify the function doesn't try to fetch from its own domain
own_domain = settings.SITE_DOMAIN
def mock_requests_get(url, timeout=None):
# Should not be called for our own domain
if own_domain in url and "/feeds" in url:
self.fail(f"Should not fetch from own domain: {url}")
# Mock response for other relays
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"type": "Success", "data": []}
mock_response.raise_for_status = Mock()
return mock_response
# When: We run the discovery task
with patch("requests.get", side_effect=mock_requests_get):
discover_feeds_from_relay_nodes()
# Then: The function should complete without calling own domain
# (if it called own domain, the test would fail in mock_requests_get)
def test_skips_bridge_feeds_from_relay_nodes(self):
"""Test that bridge virtual feeds listed by other relays are skipped."""
# Given: A relay node listing only bridge virtual feeds
mock_relay_response = Mock()
mock_relay_response.status_code = 200
mock_relay_response.json.return_value = {
"type": "Success",
"data": [
"https://other-relay.org/bridge/rss/"
"?url=https%3A%2F%2Frss.arxiv.org%2Frss%2Fquant-ph",
"https://other-relay.org/bridge/activitypub/@user@instance.tld/",
],
}
mock_relay_response.raise_for_status = Mock()
def mock_requests_get(url, timeout=None):
if "/feeds" in url:
return mock_relay_response
# Bridge feeds must not even be validated
self.fail(f"Should not validate bridge feed: {url}")
# When: We run the discovery task
with patch("requests.get", side_effect=mock_requests_get):
discover_feeds_from_relay_nodes()
# Then: No bridge feed is registered
self.assertEqual(Feed.objects.count(), 0)