test_embed.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import os
  2. import unittest
  3. import requests
  4. from base import BaseTestCase, Media
  5. from parameterized import parameterized
  6. class Embed:
  7. container = '.tweet-embed'
  8. footer = '.embed-footer'
  9. tweet_content = '.tweet-content'
  10. tweet_header = '.tweet-header'
  11. fullname = '.fullname'
  12. username = '.username'
  13. avatar = '.avatar'
  14. stats = '.tweet-stats'
  15. quote = '.quote'
  16. error_panel = '.error-panel'
  17. class TweetEmbedTest(BaseTestCase):
  18. """Test tweet embed rendering."""
  19. tweet = 'elonmusk/status/1141367104702038016'
  20. def test_embed_container_visible(self):
  21. self.open_nitter(self.tweet + '/embed')
  22. self.assert_element_visible(Embed.container)
  23. def test_embed_has_footer(self):
  24. self.open_nitter(self.tweet + '/embed')
  25. self.assert_element_visible(Embed.footer)
  26. self.assert_text_visible('Read more on', Embed.footer)
  27. def test_embed_has_tweet_content(self):
  28. self.open_nitter(self.tweet + '/embed')
  29. self.assert_element_visible(Embed.tweet_content)
  30. def test_embed_has_avatar(self):
  31. self.open_nitter(self.tweet + '/embed')
  32. self.assert_element_visible(Embed.avatar)
  33. def test_embed_has_username(self):
  34. self.open_nitter(self.tweet + '/embed')
  35. self.assert_element_visible(Embed.username)
  36. def test_embed_has_stats(self):
  37. self.open_nitter(self.tweet + '/embed')
  38. self.assert_element_visible(Embed.stats)
  39. def test_embed_footer_links_to_tweet(self):
  40. self.open_nitter(self.tweet + '/embed')
  41. href = self.get_attribute(Embed.footer, 'href')
  42. self.assertIn('/elonmusk/status/1141367104702038016', href)
  43. class TweetEmbedMediaTest(BaseTestCase):
  44. """Test embed rendering with various media types."""
  45. def test_embed_with_image(self):
  46. self.open_nitter('mobile_test/status/519364660823207936/embed')
  47. self.assert_element_visible(Embed.container)
  48. self.scroll_to(Media.container)
  49. self.assert_element_visible(Media.image)
  50. def test_embed_with_gif(self):
  51. self.open_nitter('elonmusk/status/1141367104702038016/embed')
  52. self.assert_element_visible(Embed.container)
  53. self.scroll_to(Media.container)
  54. self.assert_element_visible(Media.gif)
  55. @unittest.skipIf(os.environ.get('GITHUB_ACTIONS') == 'true',
  56. 'TweetResultByRestId is Cloudflare-blocked from GitHub datacenter IPs')
  57. def test_embed_with_video(self):
  58. self.open_nitter('d0m96/status/1078373829917974528/embed')
  59. self.assert_element_visible(Embed.container)
  60. self.scroll_to(Media.container)
  61. self.assert_element_visible(Media.video)
  62. def test_embed_with_gallery(self):
  63. self.open_nitter('mobile_test/status/451108446603980803/embed')
  64. self.assert_element_visible(Embed.container)
  65. self.scroll_to(Media.container)
  66. self.assert_element_visible(Media.row)
  67. class TweetEmbedQuoteTest(BaseTestCase):
  68. """Test embed rendering with quoted tweets."""
  69. def test_embed_with_quote_shows_quote(self):
  70. self.open_nitter('elonmusk/status/1138827760107790336/embed')
  71. self.assert_element_visible(Embed.container)
  72. self.assert_element_visible(Embed.quote)
  73. def test_embed_quote_has_content(self):
  74. self.open_nitter('elonmusk/status/1138827760107790336/embed')
  75. quote = self.find_element(Embed.quote)
  76. self.assertIsNotNone(quote.text)
  77. class EmbedErrorTest(BaseTestCase):
  78. """Test embed error handling."""
  79. def test_nonexistent_tweet_shows_error(self):
  80. self.open_nitter('nobody/status/1/embed')
  81. self.assert_element_visible('.tweet-embed.error-embed')
  82. self.assert_text_visible('not found', Embed.error_panel)
  83. def test_protected_account_embed_shows_error(self):
  84. self.open_nitter('mobile_test_7/status/1/embed')
  85. self.assert_element_visible('.tweet-embed.error-embed')
  86. def test_invalid_tweet_id_shows_error(self):
  87. self.open_nitter('jack/status/notanumber/embed')
  88. self.assert_element_visible('.tweet-embed.error-embed')
  89. class OEmbedApiTest(BaseTestCase):
  90. """Test oEmbed API endpoint."""
  91. base_url = 'http://localhost:8080'
  92. tweet_url = 'https://twitter.com/elonmusk/status/1141367104702038016'
  93. def test_oembed_returns_json(self):
  94. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}')
  95. self.assertEqual(resp.status_code, 200)
  96. self.assertEqual(resp.headers['Content-Type'], 'application/json')
  97. def test_oembed_has_required_fields(self):
  98. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}')
  99. data = resp.json()
  100. self.assertEqual(data['type'], 'rich')
  101. self.assertEqual(data['version'], '1.0')
  102. self.assertIn('html', data)
  103. self.assertIn('author_name', data)
  104. self.assertIn('provider_name', data)
  105. def test_oembed_html_contains_iframe(self):
  106. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}')
  107. data = resp.json()
  108. self.assertIn('<iframe', data['html'])
  109. self.assertIn('/embed', data['html'])
  110. def test_oembed_has_cors_header(self):
  111. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}')
  112. self.assertEqual(resp.headers.get('Access-Control-Allow-Origin'), '*')
  113. def test_oembed_missing_url_returns_400(self):
  114. resp = requests.get(f'{self.base_url}/api/oembed')
  115. self.assertEqual(resp.status_code, 400)
  116. def test_oembed_invalid_url_returns_400(self):
  117. resp = requests.get(f'{self.base_url}/api/oembed?url=https://example.com')
  118. self.assertEqual(resp.status_code, 400)
  119. def test_oembed_supports_x_com_url(self):
  120. x_url = 'https://x.com/elonmusk/status/1141367104702038016'
  121. resp = requests.get(f'{self.base_url}/api/oembed?url={x_url}')
  122. self.assertEqual(resp.status_code, 200)
  123. def test_oembed_strips_query_params(self):
  124. url_with_params = 'https://twitter.com/elonmusk/status/1141367104702038016?s=20&t=abc'
  125. resp = requests.get(f'{self.base_url}/api/oembed?url={url_with_params}')
  126. self.assertEqual(resp.status_code, 200)
  127. data = resp.json()
  128. self.assertIn('html', data)
  129. def test_oembed_handles_trailing_slash(self):
  130. url_with_slash = 'https://twitter.com/elonmusk/status/1141367104702038016/'
  131. resp = requests.get(f'{self.base_url}/api/oembed?url={url_with_slash}')
  132. self.assertEqual(resp.status_code, 200)
  133. def test_oembed_handles_mobile_url(self):
  134. mobile_url = 'https://mobile.twitter.com/elonmusk/status/1141367104702038016'
  135. resp = requests.get(f'{self.base_url}/api/oembed?url={mobile_url}')
  136. self.assertEqual(resp.status_code, 200)
  137. def test_oembed_rejects_malformed_tweet_id(self):
  138. bad_url = 'https://twitter.com/elonmusk/status/notanumber'
  139. resp = requests.get(f'{self.base_url}/api/oembed?url={bad_url}')
  140. self.assertEqual(resp.status_code, 400)
  141. def test_oembed_maxwidth_param_accepted(self):
  142. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}&maxwidth=400')
  143. self.assertEqual(resp.status_code, 200)
  144. def test_oembed_maxwidth_clamps_to_range(self):
  145. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}&maxwidth=100')
  146. data = resp.json()
  147. self.assertEqual(data['width'], 220)
  148. self.assertIn('max-width:220px', data['html'])
  149. def test_oembed_maxwidth_caps_at_550(self):
  150. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}&maxwidth=9999')
  151. data = resp.json()
  152. self.assertEqual(data['width'], 550)
  153. def test_oembed_maxwidth_invalid_uses_default(self):
  154. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}&maxwidth=abc')
  155. data = resp.json()
  156. self.assertEqual(data['width'], 550)
  157. def test_oembed_author_url_present(self):
  158. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}')
  159. data = resp.json()
  160. self.assertIn('author_url', data)
  161. self.assertIn('elonmusk', data['author_url'])
  162. def test_oembed_accepts_nitter_url(self):
  163. nitter_url = f'{self.base_url}/elonmusk/status/1141367104702038016'
  164. resp = requests.get(f'{self.base_url}/api/oembed?url={nitter_url}')
  165. self.assertEqual(resp.status_code, 200)
  166. data = resp.json()
  167. self.assertIn('html', data)
  168. def test_oembed_format_json_accepted(self):
  169. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}&format=json')
  170. self.assertEqual(resp.status_code, 200)
  171. def test_oembed_format_xml_returns_501(self):
  172. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}&format=xml')
  173. self.assertEqual(resp.status_code, 501)
  174. def test_oembed_has_title(self):
  175. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}')
  176. data = resp.json()
  177. self.assertIn('title', data)
  178. self.assertIsInstance(data['title'], str)
  179. self.assertGreater(len(data['title']), 0)
  180. def test_oembed_has_null_height(self):
  181. resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}')
  182. data = resp.json()
  183. self.assertIsNone(data['height'])
  184. class OEmbedDiscoveryTest(BaseTestCase):
  185. """Test oEmbed discovery link tags on tweet pages."""
  186. base_url = 'http://localhost:8080'
  187. def test_tweet_page_has_oembed_link_tag(self):
  188. self.open_nitter('elonmusk/status/1141367104702038016')
  189. self.assert_element_present('link[type="application/json+oembed"]')
  190. def test_oembed_link_tag_points_to_api(self):
  191. resp = requests.get(f'{self.base_url}/elonmusk/status/1141367104702038016')
  192. self.assertIn('application/json+oembed', resp.text)
  193. self.assertIn('/api/oembed?url=', resp.text)
  194. self.assertIn('1141367104702038016', resp.text)
  195. def test_oembed_discovery_roundtrip(self):
  196. """Fetch a tweet page, extract oEmbed URL, call it, verify response."""
  197. import re
  198. resp = requests.get(f'{self.base_url}/elonmusk/status/1141367104702038016')
  199. match = re.search(
  200. r'type="application/json\+oembed"\s+href="([^"]*)"', resp.text)
  201. self.assertIsNotNone(match, "No oEmbed discovery link found in page")
  202. oembed_url = match.group(1).replace('&amp;', '&')
  203. oembed_resp = requests.get(oembed_url)
  204. self.assertEqual(oembed_resp.status_code, 200)
  205. data = oembed_resp.json()
  206. self.assertEqual(data['type'], 'rich')
  207. self.assertIn('html', data)
  208. @unittest.skipIf(os.environ.get('GITHUB_ACTIONS') == 'true',
  209. 'TweetResultByRestId is Cloudflare-blocked from GitHub datacenter IPs')
  210. class VideoEmbedTest(BaseTestCase):
  211. """Test video embed route (/i/videos/tweet/{id})."""
  212. video_tweet_id = '1078373829917974528'
  213. def test_video_embed_has_video_element(self):
  214. self.open_nitter(f'i/videos/tweet/{self.video_tweet_id}')
  215. self.assert_element_visible('video')
  216. def test_video_embed_has_poster(self):
  217. self.open_nitter(f'i/videos/tweet/{self.video_tweet_id}')
  218. poster = self.get_attribute('video', 'poster')
  219. self.assertIsNotNone(poster)
  220. self.assertIn('pic/', poster)
  221. def test_video_embed_nonexistent_returns_error(self):
  222. self.open_nitter('i/videos/tweet/1')
  223. self.assert_element_visible('.error-embed')