tests.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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. 'comments': '/blog/1/comments',
  120. })
  121. self.assertCategories(items[0], ['python', 'testing'])
  122. for item in items:
  123. self.assertChildNodes(item, [
  124. 'title',
  125. 'link',
  126. 'description',
  127. 'guid',
  128. 'category',
  129. 'pubDate',
  130. 'author',
  131. 'comments',
  132. ])
  133. # Assert that <guid> does not have any 'isPermaLink' attribute
  134. self.assertIsNone(item.getElementsByTagName(
  135. 'guid')[0].attributes.get('isPermaLink'))
  136. def test_rss2_feed_guid_permalink_false(self):
  137. """
  138. Test if the 'isPermaLink' attribute of <guid> element of an item
  139. in the RSS feed is 'false'.
  140. """
  141. response = self.client.get(
  142. '/syndication/rss2/guid_ispermalink_false/')
  143. doc = minidom.parseString(response.content)
  144. chan = doc.getElementsByTagName(
  145. 'rss')[0].getElementsByTagName('channel')[0]
  146. items = chan.getElementsByTagName('item')
  147. for item in items:
  148. self.assertEqual(
  149. item.getElementsByTagName('guid')[0].attributes.get(
  150. 'isPermaLink').value, "false")
  151. def test_rss2_feed_guid_permalink_true(self):
  152. """
  153. Test if the 'isPermaLink' attribute of <guid> element of an item
  154. in the RSS feed is 'true'.
  155. """
  156. response = self.client.get(
  157. '/syndication/rss2/guid_ispermalink_true/')
  158. doc = minidom.parseString(response.content)
  159. chan = doc.getElementsByTagName(
  160. 'rss')[0].getElementsByTagName('channel')[0]
  161. items = chan.getElementsByTagName('item')
  162. for item in items:
  163. self.assertEqual(
  164. item.getElementsByTagName('guid')[0].attributes.get(
  165. 'isPermaLink').value, "true")
  166. def test_rss2_single_enclosure(self):
  167. response = self.client.get('/syndication/rss2/single-enclosure/')
  168. doc = minidom.parseString(response.content)
  169. chan = doc.getElementsByTagName('rss')[0].getElementsByTagName('channel')[0]
  170. items = chan.getElementsByTagName('item')
  171. for item in items:
  172. enclosures = item.getElementsByTagName('enclosure')
  173. self.assertEqual(len(enclosures), 1)
  174. def test_rss2_multiple_enclosures(self):
  175. with self.assertRaisesMessage(
  176. ValueError,
  177. "RSS feed items may only have one enclosure, see "
  178. "http://www.rssboard.org/rss-profile#element-channel-item-enclosure"
  179. ):
  180. self.client.get('/syndication/rss2/multiple-enclosure/')
  181. def test_rss091_feed(self):
  182. """
  183. Test the structure and content of feeds generated by RssUserland091Feed.
  184. """
  185. response = self.client.get('/syndication/rss091/')
  186. doc = minidom.parseString(response.content)
  187. # Making sure there's only 1 `rss` element and that the correct
  188. # RSS version was specified.
  189. feed_elem = doc.getElementsByTagName('rss')
  190. self.assertEqual(len(feed_elem), 1)
  191. feed = feed_elem[0]
  192. self.assertEqual(feed.getAttribute('version'), '0.91')
  193. # Making sure there's only one `channel` element w/in the
  194. # `rss` element.
  195. chan_elem = feed.getElementsByTagName('channel')
  196. self.assertEqual(len(chan_elem), 1)
  197. chan = chan_elem[0]
  198. self.assertChildNodes(
  199. chan, [
  200. 'title', 'link', 'description', 'language', 'lastBuildDate',
  201. 'item', 'atom:link', 'ttl', 'copyright', 'category',
  202. ]
  203. )
  204. # Ensure the content of the channel is correct
  205. self.assertChildNodeContent(chan, {
  206. 'title': 'My blog',
  207. 'link': 'http://example.com/blog/',
  208. })
  209. self.assertCategories(chan, ['python', 'django'])
  210. # Check feed_url is passed
  211. self.assertEqual(
  212. chan.getElementsByTagName('atom:link')[0].getAttribute('href'),
  213. 'http://example.com/syndication/rss091/'
  214. )
  215. items = chan.getElementsByTagName('item')
  216. self.assertEqual(len(items), Entry.objects.count())
  217. self.assertChildNodeContent(items[0], {
  218. 'title': 'My first entry',
  219. 'description': 'Overridden description: My first entry',
  220. 'link': 'http://example.com/blog/1/',
  221. })
  222. for item in items:
  223. self.assertChildNodes(item, ['title', 'link', 'description'])
  224. self.assertCategories(item, [])
  225. def test_atom_feed(self):
  226. """
  227. Test the structure and content of feeds generated by Atom1Feed.
  228. """
  229. response = self.client.get('/syndication/atom/')
  230. feed = minidom.parseString(response.content).firstChild
  231. self.assertEqual(feed.nodeName, 'feed')
  232. self.assertEqual(feed.getAttribute('xmlns'), 'http://www.w3.org/2005/Atom')
  233. self.assertChildNodes(
  234. feed,
  235. ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'rights', 'category', 'author']
  236. )
  237. for link in feed.getElementsByTagName('link'):
  238. if link.getAttribute('rel') == 'self':
  239. self.assertEqual(link.getAttribute('href'), 'http://example.com/syndication/atom/')
  240. entries = feed.getElementsByTagName('entry')
  241. self.assertEqual(len(entries), Entry.objects.count())
  242. for entry in entries:
  243. self.assertChildNodes(entry, [
  244. 'title',
  245. 'link',
  246. 'id',
  247. 'summary',
  248. 'category',
  249. 'updated',
  250. 'published',
  251. 'rights',
  252. 'author',
  253. ])
  254. summary = entry.getElementsByTagName('summary')[0]
  255. self.assertEqual(summary.getAttribute('type'), 'html')
  256. def test_atom_feed_published_and_updated_elements(self):
  257. """
  258. The published and updated elements are not
  259. the same and now adhere to RFC 4287.
  260. """
  261. response = self.client.get('/syndication/atom/')
  262. feed = minidom.parseString(response.content).firstChild
  263. entries = feed.getElementsByTagName('entry')
  264. published = entries[0].getElementsByTagName('published')[0].firstChild.wholeText
  265. updated = entries[0].getElementsByTagName('updated')[0].firstChild.wholeText
  266. self.assertNotEqual(published, updated)
  267. def test_atom_single_enclosure(self):
  268. response = self.client.get('/syndication/atom/single-enclosure/')
  269. feed = minidom.parseString(response.content).firstChild
  270. items = feed.getElementsByTagName('entry')
  271. for item in items:
  272. links = item.getElementsByTagName('link')
  273. links = [link for link in links if link.getAttribute('rel') == 'enclosure']
  274. self.assertEqual(len(links), 1)
  275. def test_atom_multiple_enclosures(self):
  276. response = self.client.get('/syndication/atom/multiple-enclosure/')
  277. feed = minidom.parseString(response.content).firstChild
  278. items = feed.getElementsByTagName('entry')
  279. for item in items:
  280. links = item.getElementsByTagName('link')
  281. links = [link for link in links if link.getAttribute('rel') == 'enclosure']
  282. self.assertEqual(len(links), 2)
  283. def test_latest_post_date(self):
  284. """
  285. Both the published and updated dates are
  286. considered when determining the latest post date.
  287. """
  288. # this feed has a `published` element with the latest date
  289. response = self.client.get('/syndication/atom/')
  290. feed = minidom.parseString(response.content).firstChild
  291. updated = feed.getElementsByTagName('updated')[0].firstChild.wholeText
  292. d = Entry.objects.latest('published').published
  293. latest_published = rfc3339_date(timezone.make_aware(d, TZ))
  294. self.assertEqual(updated, latest_published)
  295. # this feed has an `updated` element with the latest date
  296. response = self.client.get('/syndication/latest/')
  297. feed = minidom.parseString(response.content).firstChild
  298. updated = feed.getElementsByTagName('updated')[0].firstChild.wholeText
  299. d = Entry.objects.exclude(pk=5).latest('updated').updated
  300. latest_updated = rfc3339_date(timezone.make_aware(d, TZ))
  301. self.assertEqual(updated, latest_updated)
  302. def test_custom_feed_generator(self):
  303. response = self.client.get('/syndication/custom/')
  304. feed = minidom.parseString(response.content).firstChild
  305. self.assertEqual(feed.nodeName, 'feed')
  306. self.assertEqual(feed.getAttribute('django'), 'rocks')
  307. self.assertChildNodes(
  308. feed,
  309. ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'spam', 'rights', 'category', 'author']
  310. )
  311. entries = feed.getElementsByTagName('entry')
  312. self.assertEqual(len(entries), Entry.objects.count())
  313. for entry in entries:
  314. self.assertEqual(entry.getAttribute('bacon'), 'yum')
  315. self.assertChildNodes(entry, [
  316. 'title',
  317. 'link',
  318. 'id',
  319. 'summary',
  320. 'ministry',
  321. 'rights',
  322. 'author',
  323. 'updated',
  324. 'published',
  325. 'category',
  326. ])
  327. summary = entry.getElementsByTagName('summary')[0]
  328. self.assertEqual(summary.getAttribute('type'), 'html')
  329. def test_feed_generator_language_attribute(self):
  330. response = self.client.get('/syndication/language/')
  331. feed = minidom.parseString(response.content).firstChild
  332. self.assertEqual(feed.firstChild.getElementsByTagName('language')[0].firstChild.nodeValue, 'de')
  333. def test_title_escaping(self):
  334. """
  335. Titles are escaped correctly in RSS feeds.
  336. """
  337. response = self.client.get('/syndication/rss2/')
  338. doc = minidom.parseString(response.content)
  339. for item in doc.getElementsByTagName('item'):
  340. link = item.getElementsByTagName('link')[0]
  341. if link.firstChild.wholeText == 'http://example.com/blog/4/':
  342. title = item.getElementsByTagName('title')[0]
  343. self.assertEqual(title.firstChild.wholeText, 'A &amp; B &lt; C &gt; D')
  344. def test_naive_datetime_conversion(self):
  345. """
  346. Datetimes are correctly converted to the local time zone.
  347. """
  348. # Naive date times passed in get converted to the local time zone, so
  349. # check the received zone offset against the local offset.
  350. response = self.client.get('/syndication/naive-dates/')
  351. doc = minidom.parseString(response.content)
  352. updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
  353. d = Entry.objects.latest('published').published
  354. latest = rfc3339_date(timezone.make_aware(d, TZ))
  355. self.assertEqual(updated, latest)
  356. def test_aware_datetime_conversion(self):
  357. """
  358. Datetimes with timezones don't get trodden on.
  359. """
  360. response = self.client.get('/syndication/aware-dates/')
  361. doc = minidom.parseString(response.content)
  362. published = doc.getElementsByTagName('published')[0].firstChild.wholeText
  363. self.assertEqual(published[-6:], '+00:42')
  364. @requires_tz_support
  365. def test_feed_last_modified_time_naive_date(self):
  366. """
  367. Tests the Last-Modified header with naive publication dates.
  368. """
  369. response = self.client.get('/syndication/naive-dates/')
  370. self.assertEqual(response.headers['Last-Modified'], 'Tue, 26 Mar 2013 01:00:00 GMT')
  371. def test_feed_last_modified_time(self):
  372. """
  373. Tests the Last-Modified header with aware publication dates.
  374. """
  375. response = self.client.get('/syndication/aware-dates/')
  376. self.assertEqual(response.headers['Last-Modified'], 'Mon, 25 Mar 2013 19:18:00 GMT')
  377. # No last-modified when feed has no item_pubdate
  378. response = self.client.get('/syndication/no_pubdate/')
  379. self.assertFalse(response.has_header('Last-Modified'))
  380. def test_feed_url(self):
  381. """
  382. The feed_url can be overridden.
  383. """
  384. response = self.client.get('/syndication/feedurl/')
  385. doc = minidom.parseString(response.content)
  386. for link in doc.getElementsByTagName('link'):
  387. if link.getAttribute('rel') == 'self':
  388. self.assertEqual(link.getAttribute('href'), 'http://example.com/customfeedurl/')
  389. def test_secure_urls(self):
  390. """
  391. Test URLs are prefixed with https:// when feed is requested over HTTPS.
  392. """
  393. response = self.client.get('/syndication/rss2/', **{
  394. 'wsgi.url_scheme': 'https',
  395. })
  396. doc = minidom.parseString(response.content)
  397. chan = doc.getElementsByTagName('channel')[0]
  398. self.assertEqual(
  399. chan.getElementsByTagName('link')[0].firstChild.wholeText[0:5],
  400. 'https'
  401. )
  402. atom_link = chan.getElementsByTagName('atom:link')[0]
  403. self.assertEqual(atom_link.getAttribute('href')[0:5], 'https')
  404. for link in doc.getElementsByTagName('link'):
  405. if link.getAttribute('rel') == 'self':
  406. self.assertEqual(link.getAttribute('href')[0:5], 'https')
  407. def test_item_link_error(self):
  408. """
  409. An ImproperlyConfigured is raised if no link could be found for the
  410. item(s).
  411. """
  412. msg = (
  413. 'Give your Article class a get_absolute_url() method, or define '
  414. 'an item_link() method in your Feed class.'
  415. )
  416. with self.assertRaisesMessage(ImproperlyConfigured, msg):
  417. self.client.get('/syndication/articles/')
  418. def test_template_feed(self):
  419. """
  420. The item title and description can be overridden with templates.
  421. """
  422. response = self.client.get('/syndication/template/')
  423. doc = minidom.parseString(response.content)
  424. feed = doc.getElementsByTagName('rss')[0]
  425. chan = feed.getElementsByTagName('channel')[0]
  426. items = chan.getElementsByTagName('item')
  427. self.assertChildNodeContent(items[0], {
  428. 'title': 'Title in your templates: My first entry\n',
  429. 'description': 'Description in your templates: My first entry\n',
  430. 'link': 'http://example.com/blog/1/',
  431. })
  432. def test_template_context_feed(self):
  433. """
  434. Custom context data can be passed to templates for title
  435. and description.
  436. """
  437. response = self.client.get('/syndication/template_context/')
  438. doc = minidom.parseString(response.content)
  439. feed = doc.getElementsByTagName('rss')[0]
  440. chan = feed.getElementsByTagName('channel')[0]
  441. items = chan.getElementsByTagName('item')
  442. self.assertChildNodeContent(items[0], {
  443. 'title': 'My first entry (foo is bar)\n',
  444. 'description': 'My first entry (foo is bar)\n',
  445. })
  446. def test_add_domain(self):
  447. """
  448. add_domain() prefixes domains onto the correct URLs.
  449. """
  450. prefix_domain_mapping = (
  451. (('example.com', '/foo/?arg=value'), 'http://example.com/foo/?arg=value'),
  452. (('example.com', '/foo/?arg=value', True), 'https://example.com/foo/?arg=value'),
  453. (('example.com', 'http://djangoproject.com/doc/'), 'http://djangoproject.com/doc/'),
  454. (('example.com', 'https://djangoproject.com/doc/'), 'https://djangoproject.com/doc/'),
  455. (('example.com', 'mailto:uhoh@djangoproject.com'), 'mailto:uhoh@djangoproject.com'),
  456. (('example.com', '//example.com/foo/?arg=value'), 'http://example.com/foo/?arg=value'),
  457. )
  458. for prefix in prefix_domain_mapping:
  459. with self.subTest(prefix=prefix):
  460. self.assertEqual(views.add_domain(*prefix[0]), prefix[1])