tests.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. import datetime
  2. from xml.dom import minidom
  3. from django.contrib.sites.models import Site
  4. from django.contrib.syndication import views
  5. from django.core.exceptions import ImproperlyConfigured
  6. from django.test import TestCase, override_settings
  7. from django.test.utils import requires_tz_support
  8. from django.utils import timezone
  9. from django.utils.feedgenerator import rfc2822_date, rfc3339_date
  10. from .models import Article, Entry
  11. TZ = timezone.get_default_timezone()
  12. class FeedTestCase(TestCase):
  13. @classmethod
  14. def setUpTestData(cls):
  15. cls.e1 = Entry.objects.create(
  16. title='My first entry', updated=datetime.datetime(1980, 1, 1, 12, 30),
  17. published=datetime.datetime(1986, 9, 25, 20, 15, 00)
  18. )
  19. cls.e2 = Entry.objects.create(
  20. title='My second entry', updated=datetime.datetime(2008, 1, 2, 12, 30),
  21. published=datetime.datetime(2006, 3, 17, 18, 0)
  22. )
  23. cls.e3 = Entry.objects.create(
  24. title='My third entry', updated=datetime.datetime(2008, 1, 2, 13, 30),
  25. published=datetime.datetime(2005, 6, 14, 10, 45)
  26. )
  27. cls.e4 = Entry.objects.create(
  28. title='A & B < C > D', updated=datetime.datetime(2008, 1, 3, 13, 30),
  29. published=datetime.datetime(2005, 11, 25, 12, 11, 23)
  30. )
  31. cls.e5 = Entry.objects.create(
  32. title='My last entry', updated=datetime.datetime(2013, 1, 20, 0, 0),
  33. published=datetime.datetime(2013, 3, 25, 20, 0)
  34. )
  35. cls.a1 = Article.objects.create(title='My first article', entry=cls.e1)
  36. def assertChildNodes(self, elem, expected):
  37. actual = {n.nodeName for n in elem.childNodes}
  38. expected = set(expected)
  39. self.assertEqual(actual, expected)
  40. def assertChildNodeContent(self, elem, expected):
  41. for k, v in expected.items():
  42. self.assertEqual(
  43. elem.getElementsByTagName(k)[0].firstChild.wholeText, v)
  44. def assertCategories(self, elem, expected):
  45. self.assertEqual(
  46. {i.firstChild.wholeText for i in elem.childNodes if i.nodeName == 'category'},
  47. set(expected)
  48. )
  49. @override_settings(ROOT_URLCONF='syndication_tests.urls')
  50. class SyndicationFeedTest(FeedTestCase):
  51. """
  52. Tests for the high-level syndication feed framework.
  53. """
  54. @classmethod
  55. def setUpClass(cls):
  56. super().setUpClass()
  57. # This cleanup is necessary because contrib.sites cache
  58. # makes tests interfere with each other, see #11505
  59. Site.objects.clear_cache()
  60. def test_rss2_feed(self):
  61. """
  62. Test the structure and content of feeds generated by Rss201rev2Feed.
  63. """
  64. response = self.client.get('/syndication/rss2/')
  65. doc = minidom.parseString(response.content)
  66. # Making sure there's only 1 `rss` element and that the correct
  67. # RSS version was specified.
  68. feed_elem = doc.getElementsByTagName('rss')
  69. self.assertEqual(len(feed_elem), 1)
  70. feed = feed_elem[0]
  71. self.assertEqual(feed.getAttribute('version'), '2.0')
  72. self.assertEqual(feed.getElementsByTagName('language')[0].firstChild.nodeValue, 'en')
  73. # Making sure there's only one `channel` element w/in the
  74. # `rss` element.
  75. chan_elem = feed.getElementsByTagName('channel')
  76. self.assertEqual(len(chan_elem), 1)
  77. chan = chan_elem[0]
  78. # Find the last build date
  79. d = Entry.objects.latest('published').published
  80. last_build_date = rfc2822_date(timezone.make_aware(d, TZ))
  81. self.assertChildNodes(
  82. chan, [
  83. 'title', 'link', 'description', 'language', 'lastBuildDate',
  84. 'item', 'atom:link', 'ttl', 'copyright', 'category',
  85. ]
  86. )
  87. self.assertChildNodeContent(chan, {
  88. 'title': 'My blog',
  89. 'description': 'A more thorough description of my blog.',
  90. 'link': 'http://example.com/blog/',
  91. 'language': 'en',
  92. 'lastBuildDate': last_build_date,
  93. 'ttl': '600',
  94. 'copyright': 'Copyright (c) 2007, Sally Smith',
  95. })
  96. self.assertCategories(chan, ['python', 'django'])
  97. # Ensure the content of the channel is correct
  98. self.assertChildNodeContent(chan, {
  99. 'title': 'My blog',
  100. 'link': 'http://example.com/blog/',
  101. })
  102. # Check feed_url is passed
  103. self.assertEqual(
  104. chan.getElementsByTagName('atom:link')[0].getAttribute('href'),
  105. 'http://example.com/syndication/rss2/'
  106. )
  107. # Find the pubdate of the first feed item
  108. d = Entry.objects.get(pk=1).published
  109. pub_date = rfc2822_date(timezone.make_aware(d, TZ))
  110. items = chan.getElementsByTagName('item')
  111. self.assertEqual(len(items), Entry.objects.count())
  112. self.assertChildNodeContent(items[0], {
  113. 'title': 'My first entry',
  114. 'description': 'Overridden description: My first entry',
  115. 'link': 'http://example.com/blog/1/',
  116. 'guid': 'http://example.com/blog/1/',
  117. 'pubDate': pub_date,
  118. 'author': 'test@example.com (Sally Smith)',
  119. })
  120. self.assertCategories(items[0], ['python', 'testing'])
  121. for item in items:
  122. self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'category', 'pubDate', 'author'])
  123. # Assert that <guid> does not have any 'isPermaLink' attribute
  124. self.assertIsNone(item.getElementsByTagName(
  125. 'guid')[0].attributes.get('isPermaLink'))
  126. def test_rss2_feed_guid_permalink_false(self):
  127. """
  128. Test if the 'isPermaLink' attribute of <guid> element of an item
  129. in the RSS feed is 'false'.
  130. """
  131. response = self.client.get(
  132. '/syndication/rss2/guid_ispermalink_false/')
  133. doc = minidom.parseString(response.content)
  134. chan = doc.getElementsByTagName(
  135. 'rss')[0].getElementsByTagName('channel')[0]
  136. items = chan.getElementsByTagName('item')
  137. for item in items:
  138. self.assertEqual(
  139. item.getElementsByTagName('guid')[0].attributes.get(
  140. 'isPermaLink').value, "false")
  141. def test_rss2_feed_guid_permalink_true(self):
  142. """
  143. Test if the 'isPermaLink' attribute of <guid> element of an item
  144. in the RSS feed is 'true'.
  145. """
  146. response = self.client.get(
  147. '/syndication/rss2/guid_ispermalink_true/')
  148. doc = minidom.parseString(response.content)
  149. chan = doc.getElementsByTagName(
  150. 'rss')[0].getElementsByTagName('channel')[0]
  151. items = chan.getElementsByTagName('item')
  152. for item in items:
  153. self.assertEqual(
  154. item.getElementsByTagName('guid')[0].attributes.get(
  155. 'isPermaLink').value, "true")
  156. def test_rss2_single_enclosure(self):
  157. response = self.client.get('/syndication/rss2/single-enclosure/')
  158. doc = minidom.parseString(response.content)
  159. chan = doc.getElementsByTagName('rss')[0].getElementsByTagName('channel')[0]
  160. items = chan.getElementsByTagName('item')
  161. for item in items:
  162. enclosures = item.getElementsByTagName('enclosure')
  163. self.assertEqual(len(enclosures), 1)
  164. def test_rss2_multiple_enclosures(self):
  165. with self.assertRaisesMessage(
  166. ValueError,
  167. "RSS feed items may only have one enclosure, see "
  168. "http://www.rssboard.org/rss-profile#element-channel-item-enclosure"
  169. ):
  170. self.client.get('/syndication/rss2/multiple-enclosure/')
  171. def test_rss091_feed(self):
  172. """
  173. Test the structure and content of feeds generated by RssUserland091Feed.
  174. """
  175. response = self.client.get('/syndication/rss091/')
  176. doc = minidom.parseString(response.content)
  177. # Making sure there's only 1 `rss` element and that the correct
  178. # RSS version was specified.
  179. feed_elem = doc.getElementsByTagName('rss')
  180. self.assertEqual(len(feed_elem), 1)
  181. feed = feed_elem[0]
  182. self.assertEqual(feed.getAttribute('version'), '0.91')
  183. # Making sure there's only one `channel` element w/in the
  184. # `rss` element.
  185. chan_elem = feed.getElementsByTagName('channel')
  186. self.assertEqual(len(chan_elem), 1)
  187. chan = chan_elem[0]
  188. self.assertChildNodes(
  189. chan, [
  190. 'title', 'link', 'description', 'language', 'lastBuildDate',
  191. 'item', 'atom:link', 'ttl', 'copyright', 'category',
  192. ]
  193. )
  194. # Ensure the content of the channel is correct
  195. self.assertChildNodeContent(chan, {
  196. 'title': 'My blog',
  197. 'link': 'http://example.com/blog/',
  198. })
  199. self.assertCategories(chan, ['python', 'django'])
  200. # Check feed_url is passed
  201. self.assertEqual(
  202. chan.getElementsByTagName('atom:link')[0].getAttribute('href'),
  203. 'http://example.com/syndication/rss091/'
  204. )
  205. items = chan.getElementsByTagName('item')
  206. self.assertEqual(len(items), Entry.objects.count())
  207. self.assertChildNodeContent(items[0], {
  208. 'title': 'My first entry',
  209. 'description': 'Overridden description: My first entry',
  210. 'link': 'http://example.com/blog/1/',
  211. })
  212. for item in items:
  213. self.assertChildNodes(item, ['title', 'link', 'description'])
  214. self.assertCategories(item, [])
  215. def test_atom_feed(self):
  216. """
  217. Test the structure and content of feeds generated by Atom1Feed.
  218. """
  219. response = self.client.get('/syndication/atom/')
  220. feed = minidom.parseString(response.content).firstChild
  221. self.assertEqual(feed.nodeName, 'feed')
  222. self.assertEqual(feed.getAttribute('xmlns'), 'http://www.w3.org/2005/Atom')
  223. self.assertChildNodes(
  224. feed,
  225. ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'rights', 'category', 'author']
  226. )
  227. for link in feed.getElementsByTagName('link'):
  228. if link.getAttribute('rel') == 'self':
  229. self.assertEqual(link.getAttribute('href'), 'http://example.com/syndication/atom/')
  230. entries = feed.getElementsByTagName('entry')
  231. self.assertEqual(len(entries), Entry.objects.count())
  232. for entry in entries:
  233. self.assertChildNodes(entry, [
  234. 'title',
  235. 'link',
  236. 'id',
  237. 'summary',
  238. 'category',
  239. 'updated',
  240. 'published',
  241. 'rights',
  242. 'author',
  243. ])
  244. summary = entry.getElementsByTagName('summary')[0]
  245. self.assertEqual(summary.getAttribute('type'), 'html')
  246. def test_atom_feed_published_and_updated_elements(self):
  247. """
  248. The published and updated elements are not
  249. the same and now adhere to RFC 4287.
  250. """
  251. response = self.client.get('/syndication/atom/')
  252. feed = minidom.parseString(response.content).firstChild
  253. entries = feed.getElementsByTagName('entry')
  254. published = entries[0].getElementsByTagName('published')[0].firstChild.wholeText
  255. updated = entries[0].getElementsByTagName('updated')[0].firstChild.wholeText
  256. self.assertNotEqual(published, updated)
  257. def test_atom_single_enclosure(self):
  258. response = self.client.get('/syndication/atom/single-enclosure/')
  259. feed = minidom.parseString(response.content).firstChild
  260. items = feed.getElementsByTagName('entry')
  261. for item in items:
  262. links = item.getElementsByTagName('link')
  263. links = [link for link in links if link.getAttribute('rel') == 'enclosure']
  264. self.assertEqual(len(links), 1)
  265. def test_atom_multiple_enclosures(self):
  266. response = self.client.get('/syndication/atom/multiple-enclosure/')
  267. feed = minidom.parseString(response.content).firstChild
  268. items = feed.getElementsByTagName('entry')
  269. for item in items:
  270. links = item.getElementsByTagName('link')
  271. links = [link for link in links if link.getAttribute('rel') == 'enclosure']
  272. self.assertEqual(len(links), 2)
  273. def test_latest_post_date(self):
  274. """
  275. Both the published and updated dates are
  276. considered when determining the latest post date.
  277. """
  278. # this feed has a `published` element with the latest date
  279. response = self.client.get('/syndication/atom/')
  280. feed = minidom.parseString(response.content).firstChild
  281. updated = feed.getElementsByTagName('updated')[0].firstChild.wholeText
  282. d = Entry.objects.latest('published').published
  283. latest_published = rfc3339_date(timezone.make_aware(d, TZ))
  284. self.assertEqual(updated, latest_published)
  285. # this feed has an `updated` element with the latest date
  286. response = self.client.get('/syndication/latest/')
  287. feed = minidom.parseString(response.content).firstChild
  288. updated = feed.getElementsByTagName('updated')[0].firstChild.wholeText
  289. d = Entry.objects.exclude(pk=5).latest('updated').updated
  290. latest_updated = rfc3339_date(timezone.make_aware(d, TZ))
  291. self.assertEqual(updated, latest_updated)
  292. def test_custom_feed_generator(self):
  293. response = self.client.get('/syndication/custom/')
  294. feed = minidom.parseString(response.content).firstChild
  295. self.assertEqual(feed.nodeName, 'feed')
  296. self.assertEqual(feed.getAttribute('django'), 'rocks')
  297. self.assertChildNodes(
  298. feed,
  299. ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'spam', 'rights', 'category', 'author']
  300. )
  301. entries = feed.getElementsByTagName('entry')
  302. self.assertEqual(len(entries), Entry.objects.count())
  303. for entry in entries:
  304. self.assertEqual(entry.getAttribute('bacon'), 'yum')
  305. self.assertChildNodes(entry, [
  306. 'title',
  307. 'link',
  308. 'id',
  309. 'summary',
  310. 'ministry',
  311. 'rights',
  312. 'author',
  313. 'updated',
  314. 'published',
  315. 'category',
  316. ])
  317. summary = entry.getElementsByTagName('summary')[0]
  318. self.assertEqual(summary.getAttribute('type'), 'html')
  319. def test_feed_generator_language_attribute(self):
  320. response = self.client.get('/syndication/language/')
  321. feed = minidom.parseString(response.content).firstChild
  322. self.assertEqual(feed.firstChild.getElementsByTagName('language')[0].firstChild.nodeValue, 'de')
  323. def test_title_escaping(self):
  324. """
  325. Titles are escaped correctly in RSS feeds.
  326. """
  327. response = self.client.get('/syndication/rss2/')
  328. doc = minidom.parseString(response.content)
  329. for item in doc.getElementsByTagName('item'):
  330. link = item.getElementsByTagName('link')[0]
  331. if link.firstChild.wholeText == 'http://example.com/blog/4/':
  332. title = item.getElementsByTagName('title')[0]
  333. self.assertEqual(title.firstChild.wholeText, 'A &amp; B &lt; C &gt; D')
  334. def test_naive_datetime_conversion(self):
  335. """
  336. Datetimes are correctly converted to the local time zone.
  337. """
  338. # Naive date times passed in get converted to the local time zone, so
  339. # check the received zone offset against the local offset.
  340. response = self.client.get('/syndication/naive-dates/')
  341. doc = minidom.parseString(response.content)
  342. updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
  343. d = Entry.objects.latest('published').published
  344. latest = rfc3339_date(timezone.make_aware(d, TZ))
  345. self.assertEqual(updated, latest)
  346. def test_aware_datetime_conversion(self):
  347. """
  348. Datetimes with timezones don't get trodden on.
  349. """
  350. response = self.client.get('/syndication/aware-dates/')
  351. doc = minidom.parseString(response.content)
  352. published = doc.getElementsByTagName('published')[0].firstChild.wholeText
  353. self.assertEqual(published[-6:], '+00:42')
  354. @requires_tz_support
  355. def test_feed_last_modified_time_naive_date(self):
  356. """
  357. Tests the Last-Modified header with naive publication dates.
  358. """
  359. response = self.client.get('/syndication/naive-dates/')
  360. self.assertEqual(response['Last-Modified'], 'Tue, 26 Mar 2013 01:00:00 GMT')
  361. def test_feed_last_modified_time(self):
  362. """
  363. Tests the Last-Modified header with aware publication dates.
  364. """
  365. response = self.client.get('/syndication/aware-dates/')
  366. self.assertEqual(response['Last-Modified'], 'Mon, 25 Mar 2013 19:18:00 GMT')
  367. # No last-modified when feed has no item_pubdate
  368. response = self.client.get('/syndication/no_pubdate/')
  369. self.assertFalse(response.has_header('Last-Modified'))
  370. def test_feed_url(self):
  371. """
  372. The feed_url can be overridden.
  373. """
  374. response = self.client.get('/syndication/feedurl/')
  375. doc = minidom.parseString(response.content)
  376. for link in doc.getElementsByTagName('link'):
  377. if link.getAttribute('rel') == 'self':
  378. self.assertEqual(link.getAttribute('href'), 'http://example.com/customfeedurl/')
  379. def test_secure_urls(self):
  380. """
  381. Test URLs are prefixed with https:// when feed is requested over HTTPS.
  382. """
  383. response = self.client.get('/syndication/rss2/', **{
  384. 'wsgi.url_scheme': 'https',
  385. })
  386. doc = minidom.parseString(response.content)
  387. chan = doc.getElementsByTagName('channel')[0]
  388. self.assertEqual(
  389. chan.getElementsByTagName('link')[0].firstChild.wholeText[0:5],
  390. 'https'
  391. )
  392. atom_link = chan.getElementsByTagName('atom:link')[0]
  393. self.assertEqual(atom_link.getAttribute('href')[0:5], 'https')
  394. for link in doc.getElementsByTagName('link'):
  395. if link.getAttribute('rel') == 'self':
  396. self.assertEqual(link.getAttribute('href')[0:5], 'https')
  397. def test_item_link_error(self):
  398. """
  399. An ImproperlyConfigured is raised if no link could be found for the
  400. item(s).
  401. """
  402. msg = (
  403. 'Give your Article class a get_absolute_url() method, or define '
  404. 'an item_link() method in your Feed class.'
  405. )
  406. with self.assertRaisesMessage(ImproperlyConfigured, msg):
  407. self.client.get('/syndication/articles/')
  408. def test_template_feed(self):
  409. """
  410. The item title and description can be overridden with templates.
  411. """
  412. response = self.client.get('/syndication/template/')
  413. doc = minidom.parseString(response.content)
  414. feed = doc.getElementsByTagName('rss')[0]
  415. chan = feed.getElementsByTagName('channel')[0]
  416. items = chan.getElementsByTagName('item')
  417. self.assertChildNodeContent(items[0], {
  418. 'title': 'Title in your templates: My first entry\n',
  419. 'description': 'Description in your templates: My first entry\n',
  420. 'link': 'http://example.com/blog/1/',
  421. })
  422. def test_template_context_feed(self):
  423. """
  424. Custom context data can be passed to templates for title
  425. and description.
  426. """
  427. response = self.client.get('/syndication/template_context/')
  428. doc = minidom.parseString(response.content)
  429. feed = doc.getElementsByTagName('rss')[0]
  430. chan = feed.getElementsByTagName('channel')[0]
  431. items = chan.getElementsByTagName('item')
  432. self.assertChildNodeContent(items[0], {
  433. 'title': 'My first entry (foo is bar)\n',
  434. 'description': 'My first entry (foo is bar)\n',
  435. })
  436. def test_add_domain(self):
  437. """
  438. add_domain() prefixes domains onto the correct URLs.
  439. """
  440. prefix_domain_mapping = (
  441. (('example.com', '/foo/?arg=value'), 'http://example.com/foo/?arg=value'),
  442. (('example.com', '/foo/?arg=value', True), 'https://example.com/foo/?arg=value'),
  443. (('example.com', 'http://djangoproject.com/doc/'), 'http://djangoproject.com/doc/'),
  444. (('example.com', 'https://djangoproject.com/doc/'), 'https://djangoproject.com/doc/'),
  445. (('example.com', 'mailto:uhoh@djangoproject.com'), 'mailto:uhoh@djangoproject.com'),
  446. (('example.com', '//example.com/foo/?arg=value'), 'http://example.com/foo/?arg=value'),
  447. )
  448. for prefix in prefix_domain_mapping:
  449. with self.subTest(prefix=prefix):
  450. self.assertEqual(views.add_domain(*prefix[0]), prefix[1])