T
' + 'Freier Anfang des Artikels. ' * 12 + '
import json
from datetime import timedelta
from unittest import mock
import feedparser
from django.contrib.auth.models import User
from django.test import TestCase
from django.utils import timezone
from podcasts.views import FeedFetch
from . import cluster, fetch, logins, refresh
from .models import FeedSettings, Item, SiteLogin, Source, Story
def rss(items, title='Testfeed', link='https://example.org/'):
body = ''.join(
f'
' + 'Ein langer Absatz mit sehr viel Text. ' * 40 + '
' class FetchTest(TestCase): def test_relative_links_become_absolute_and_scripts_go(self): out = fetch.clean_html('Hi x
'
'
Kurz 
Text
'), 'Text
') def test_short_title(self): self.assertEqual(fetch.short_title('tagesschau.de - die erste Adresse für Nachrichten'), 'tagesschau.de') self.assertEqual(fetch.short_title('Aktuell - FAZ.NET'), 'FAZ.NET') self.assertEqual(fetch.short_title('DIE ZEIT | Nachrichten, News, Hintergründe'), 'DIE ZEIT') self.assertEqual(fetch.short_title('Golem.de'), 'Golem.de') self.assertEqual(fetch.short_title('Kotlin-Blog'), 'Kotlin-Blog') def test_discover(self): feeds, icon = fetch.discover('' '', 'https://ex.org/') self.assertEqual(feeds, [('https://ex.org/feed', 'F')]) self.assertEqual(icon, 'https://ex.org/fav.png') class ClusterTest(TestCase): def setUp(self): self.u = User.objects.create_user('u', password='pw12345678') self.a = Source.objects.create(user=self.u, url='https://a/', title='A') self.b = Source.objects.create(user=self.u, url='https://b/', title='B') self.c = Source.objects.create(user=self.u, url='https://c/', title='C') def item(self, source, title, summary=''): return Item.objects.create(source=source, user=self.u, kind='news', guid_hash=title[:60], title=title, summary=summary, published=timezone.now()) def test_same_event_bundled_other_apart(self): c = cluster.Clusterer(self.u, timezone.now()) s1 = c.add(self.item(self.a, 'Oktoberfest endet mit neuem Besucherrekord in München')) s2 = c.add(self.item(self.b, 'Rekord-Wiesn: 7,4 Millionen Besucher auf dem Oktoberfest in München')) s3 = c.add(self.item(self.c, 'Bahn führt Familienticket dauerhaft ein')) self.assertEqual(s1, s2) self.assertNotEqual(s1, s3) s1.refresh_from_db() self.assertEqual((s1.size, s1.sources), (2, 2)) def test_same_source_needs_to_be_very_close(self): c = cluster.Clusterer(self.u, timezone.now()) s1 = c.add(self.item(self.a, 'Nations League: Pressekonferenz vor Irland gegen Israel abgebrochen')) s2 = c.add(self.item(self.a, 'Nations League: England schlägt Kroatien deutlich')) self.assertNotEqual(s1, s2) class ApiTest(TestCase): def setUp(self): self.u = User.objects.create_user('tester', password='pw12345678') self.client.force_login(self.u) def post(self, url, data=None): return self.client.post(url, json.dumps(data or {}), content_type='application/json') def add(self, kind, feed, url='https://example.org/feed'): with mock.patch('feeds.refresh._fetch_feed', return_value=fetched(feed, url)), \ mock.patch('feeds.fetch.fetch_article', return_value=(LONG, 'https://example.org/img.jpg', False)): r = self.post('/feeds/sources/add/', {'url': url, 'kind': kind, 'category': 'Politik'}) self.assertEqual(r.status_code, 200) return r.json()['source'] def test_requires_login(self): self.client.logout() self.assertEqual(self.client.get('/feeds/state/').status_code, 401) def test_add_news_fetches_articles_and_bundles(self): s = self.add('news', rss([{'guid': 'a', 'title': 'Merz besucht Kiew und sagt Hilfe zu', 'text': 'Kurz.'}, {'guid': 'b', 'title': 'Bahn Familienticket dauerhaft', 'text': 'Kurz.'}])) self.assertEqual(s['title'], 'Testfeed') self.assertEqual(s['unread'], 2) items = Item.objects.filter(user=self.u) self.assertTrue(all(i.content_full for i in items)) self.assertEqual(Story.objects.filter(user=self.u).count(), 2) news = self.client.get('/feeds/news/').json() self.assertEqual(len(news['stories']), 2) self.assertEqual(news['stories'][0]['category'], 'Politik') b = self.client.get('/feeds/briefing/').json() self.assertEqual(b['total'], 2) self.assertEqual(b['sections'][0]['category'], 'Politik') self.post('/feeds/briefing/seen/') self.assertFalse(Item.objects.filter(user=self.u, read_at=None).exists()) self.assertEqual(self.client.get('/feeds/briefing/').json()['total'], 0) def test_blog_keeps_full_feed_text_and_lists(self): self.add('blog', rss([{'guid': 'p1', 'title': 'Ein Artikel', 'text': LONG}])) item = Item.objects.get(user=self.u) self.assertFalse(item.content_full) # the feed had the whole text self.assertIsNone(item.story) blogs = self.client.get('/feeds/blogs/?unread=1').json() self.assertEqual([i['title'] for i in blogs['items']], ['Ein Artikel']) detail = self.client.get(f'/feeds/items/{item.id}/').json()['item'] self.assertIn('langer Absatz', detail['content']) self.post('/feeds/items/read/', {'ids': [item.id]}) self.assertEqual(self.client.get('/feeds/blogs/?unread=1').json()['items'], []) self.post(f'/feeds/items/{item.id}/star/', {'starred': True}) self.assertEqual(len(self.client.get('/feeds/starred/').json()['items']), 1) def test_blog_folders(self): with mock.patch('feeds.refresh._fetch_feed', return_value=fetched(rss([{'guid': 'x', 'title': 'Essay', 'text': LONG}]))): self.post('/feeds/sources/add/', {'url': 'https://a.example/feed', 'kind': 'blog', 'category': 'Philosophie'}) with mock.patch('feeds.refresh._fetch_feed', return_value=fetched(rss([{'guid': 'y', 'title': 'Code', 'text': LONG}]))): self.post('/feeds/sources/add/', {'url': 'https://b.example/feed', 'kind': 'blog', 'category': ''}) self.assertEqual(self.client.get('/feeds/state/').json()['folders'], ['Philosophie']) self.assertEqual([i['title'] for i in self.client.get('/feeds/blogs/?folder=Philosophie').json()['items']], ['Essay']) self.assertEqual([i['title'] for i in self.client.get('/feeds/blogs/?folder=').json()['items']], ['Code']) self.assertIn('text="Blogs – Philosophie"', self.client.get('/feeds/export.opml').content.decode()) def test_old_news_expires_and_is_not_taken_in_again(self): old = (timezone.now() - timedelta(days=3)).strftime('%a, %d %b %Y %H:%M:%S +0000') feed = rss([{'guid': 'new', 'title': 'Heute passiert', 'text': LONG}, {'guid': 'old', 'title': 'Vorgestern passiert', 'text': LONG, 'date': old}]) src = self.add('news', feed) self.assertEqual(Item.objects.filter(user=self.u).count(), 1) # too old to take in Item.objects.filter(user=self.u).update(published=timezone.now() - timedelta(days=3)) refresh.expire(self.u) self.assertEqual(Item.objects.filter(user=self.u, gone=False).count(), 0) self.assertEqual(Story.objects.filter(user=self.u).count(), 0) source = Source.objects.get(pk=src['id']) with mock.patch('feeds.refresh._fetch_feed', return_value=fetched(feed)): refresh.refresh([source]) self.assertEqual(Item.objects.filter(user=self.u, gone=False).count(), 0) def test_feed_wide_timestamp_means_first_seen(self): stamp = (timezone.now() - timedelta(hours=5)).strftime('%a, %d %b %Y %H:%M:%S +0000') self.add('news', rss([{'guid': f'g{n}', 'title': f'Meldung {n}', 'text': LONG, 'date': stamp} for n in range(6)])) for i in Item.objects.filter(user=self.u): self.assertLess((timezone.now() - i.published).total_seconds(), 600) # first fetch: feed order, minutes apart def test_new_blog_without_dates_keeps_order_and_few_unread(self): feed = (b'' + 'Freier Anfang des Artikels. ' * 12 + '
Unterstützen Sie die junge Welt mit einem Abonnement und erhalten Sie Zugang zu allen Analysen
') FULL = '' + 'Der ganze Artikel für Abonnenten. ' * 15 + '
') * 4 + 'Anriss
', published=timezone.now()) logins._sessions.clear() def post(self, url, data): return self.client.post(url, json.dumps(data), content_type='application/json') def test_encryption_round_trip_and_not_plain(self): token = logins.encrypt('geheim') self.assertNotIn('geheim', token) self.assertEqual(logins.decrypt(token), 'geheim') self.assertIsNone(logins.decrypt('kaputt')) def test_paywalled_page_keeps_free_part_and_waits(self): with mock.patch('requests.get', return_value=FakeResponse(200, text=PAYWALL)): refresh.load_article(self.item) self.item.refresh_from_db() self.assertTrue(self.item.paywalled) self.assertFalse(self.item.content_full) self.assertIn('Freier Anfang', self.item.content) self.assertFalse(refresh.needs_article(self.item, self.src)) # no login: don't try again def test_wrong_login_is_refused_and_not_stored(self): with mock.patch('requests.Session.post', return_value=FakeResponse(302, 'https://www.jungewelt.de/loginFailed.php?x')): r = self.post('/feeds/logins/', {'source': self.src.id, 'username': 'u', 'password': 'falsch'}) self.assertEqual(r.status_code, 400) self.assertIn('falsch', r.json()['error']) self.assertFalse(SiteLogin.objects.exists()) def test_login_stores_encrypted_and_refetches_full_articles(self): Item.objects.filter(pk=self.item.pk).update(paywalled=True) def login_post(session, url, **kw): session.cookies.set('jW', 'sitzung') return FakeResponse(302, 'https://www.jungewelt.de/') with mock.patch('requests.Session.post', autospec=True, side_effect=login_post), \ mock.patch('requests.Session.get', return_value=FakeResponse(200, text=FULL)): r = self.post('/feeds/logins/', {'source': self.src.id, 'username': 'leser', 'password': 'abo-passwort'}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['refetched'], 1) login = SiteLogin.objects.get(user=self.u) self.assertNotIn('abo-passwort', login.secret) self.item.refresh_from_db() self.assertTrue(self.item.content_full) self.assertFalse(self.item.paywalled) self.assertIn('ganze Artikel', self.item.content) state = self.client.get('/feeds/state/').json() info = state['sources'][0]['login'] self.assertEqual((info['username'], info['ok'], info['site']), ('leser', True, 'junge Welt')) self.assertNotIn('abo-passwort', json.dumps(state)) self.post('/feeds/logins/delete/', {'source': self.src.id}) self.assertFalse(SiteLogin.objects.exists()) def test_sources_of_other_sites_have_no_login(self): other = Source.objects.create(user=self.u, url='https://blog.example/feed', title='B', kind='blog') state = self.client.get('/feeds/state/').json() by_id = {s['id']: s for s in state['sources']} self.assertIsNone(by_id[other.id]['login']) self.assertIsNotNone(by_id[self.src.id]['login']) class BlogFeedTest(TestCase): def setUp(self): self.u = User.objects.create_user('tester', password='pw12345678') self.client.force_login(self.u) now = timezone.now() self.busy = Source.objects.create(user=self.u, url='https://busy/', title='Busy', kind='blog', category='Politik') self.rare = Source.objects.create(user=self.u, url='https://rare/', title='Rare', kind='blog', category='Politik') for n in range(10): # a post every two hours Item.objects.create(source=self.busy, user=self.u, kind='blog', guid_hash=f'b{n}', title=f'Busy {n}', content='' + 'wort ' * 460 + '
', words=460, published=now - timedelta(hours=2 * n)) Item.objects.create(source=self.rare, user=self.u, kind='blog', guid_hash='r', title='Rare essay', published=now - timedelta(hours=5)) def post(self, url, data): return self.client.post(url, json.dumps(data), content_type='application/json') def by_id(self): return {s['id']: s for s in self.client.get('/feeds/state/').json()['sources']} def test_interval_new_and_words(self): st = self.by_id() self.assertLess(st[self.busy.id]['interval_days'], 1) self.assertEqual(st[self.rare.id]['interval_days'], 30.0) # one post: counts as rare self.assertEqual(st[self.busy.id]['new'], 10) items = self.client.get('/feeds/blogs/?unread=1').json()['items'] self.assertEqual(next(i for i in items if i['title'] == 'Busy 0')['words'], 460) def test_hidden_blog_only_in_its_folder_and_not_counted(self): self.post(f'/feeds/sources/{self.busy.id}/', {'hidden': True}) state = self.client.get('/feeds/state/').json() self.assertEqual((state['blogs_unread'], state['blogs_new']), (1, 1)) main = [i['title'] for i in self.client.get('/feeds/blogs/?unread=1').json()['items']] self.assertEqual(main, ['Rare essay']) folder = self.client.get('/feeds/blogs/?unread=1&folder=Politik').json()['items'] self.assertEqual(len(folder), 11) def test_favorites_filter(self): self.post(f'/feeds/sources/{self.rare.id}/', {'fav': True}) self.assertTrue(self.by_id()[self.rare.id]['fav']) favs = [i['title'] for i in self.client.get('/feeds/blogs/?fav=1').json()['items']] self.assertEqual(favs, ['Rare essay'])