logo

youtube-dl

[mirror] Download/Watch videos from video hostersgit clone https://hacktivis.me/git/mirror/youtube-dl.git

test_http.py (23920B)


  1. #!/usr/bin/env python
  2. # coding: utf-8
  3. from __future__ import unicode_literals
  4. # Allow direct execution
  5. import os
  6. import sys
  7. import unittest
  8. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  9. import contextlib
  10. import gzip
  11. import io
  12. import ssl
  13. import tempfile
  14. import threading
  15. import zlib
  16. # avoid deprecated alias assertRaisesRegexp
  17. if hasattr(unittest.TestCase, 'assertRaisesRegex'):
  18. unittest.TestCase.assertRaisesRegexp = unittest.TestCase.assertRaisesRegex
  19. try:
  20. import brotli
  21. except ImportError:
  22. brotli = None
  23. try:
  24. from urllib.request import pathname2url
  25. except ImportError:
  26. from urllib import pathname2url
  27. from youtube_dl.compat import (
  28. compat_http_cookiejar_Cookie,
  29. compat_http_server,
  30. compat_str as str,
  31. compat_urllib_error,
  32. compat_urllib_HTTPError,
  33. compat_urllib_parse,
  34. compat_urllib_request,
  35. )
  36. from youtube_dl.utils import (
  37. sanitized_Request,
  38. update_Request,
  39. urlencode_postdata,
  40. )
  41. from test.helper import (
  42. expectedFailureIf,
  43. FakeYDL,
  44. FakeLogger,
  45. http_server_port,
  46. )
  47. from youtube_dl import YoutubeDL
  48. TEST_DIR = os.path.dirname(os.path.abspath(__file__))
  49. class HTTPTestRequestHandler(compat_http_server.BaseHTTPRequestHandler):
  50. protocol_version = 'HTTP/1.1'
  51. # work-around old/new -style class inheritance
  52. def super(self, meth_name, *args, **kwargs):
  53. from types import MethodType
  54. try:
  55. super()
  56. fn = lambda s, m, *a, **k: getattr(super(), m)(*a, **k)
  57. except TypeError:
  58. fn = lambda s, m, *a, **k: getattr(compat_http_server.BaseHTTPRequestHandler, m)(s, *a, **k)
  59. self.super = MethodType(fn, self)
  60. return self.super(meth_name, *args, **kwargs)
  61. def log_message(self, format, *args):
  62. pass
  63. def _headers(self):
  64. payload = str(self.headers).encode('utf-8')
  65. self.send_response(200)
  66. self.send_header('Content-Type', 'application/json')
  67. self.send_header('Content-Length', str(len(payload)))
  68. self.end_headers()
  69. self.wfile.write(payload)
  70. def _redirect(self):
  71. self.send_response(int(self.path[len('/redirect_'):]))
  72. self.send_header('Location', '/method')
  73. self.send_header('Content-Length', '0')
  74. self.end_headers()
  75. def _method(self, method, payload=None):
  76. self.send_response(200)
  77. self.send_header('Content-Length', str(len(payload or '')))
  78. self.send_header('Method', method)
  79. self.end_headers()
  80. if payload:
  81. self.wfile.write(payload)
  82. def _status(self, status):
  83. payload = '<html>{0} NOT FOUND</html>'.format(status).encode('utf-8')
  84. self.send_response(int(status))
  85. self.send_header('Content-Type', 'text/html; charset=utf-8')
  86. self.send_header('Content-Length', str(len(payload)))
  87. self.end_headers()
  88. self.wfile.write(payload)
  89. def _read_data(self):
  90. if 'Content-Length' in self.headers:
  91. return self.rfile.read(int(self.headers['Content-Length']))
  92. def _test_url(self, path, host='127.0.0.1', scheme='http', port=None):
  93. return '{0}://{1}:{2}/{3}'.format(
  94. scheme, host,
  95. port if port is not None
  96. else http_server_port(self.server), path)
  97. def do_POST(self):
  98. data = self._read_data()
  99. if self.path.startswith('/redirect_'):
  100. self._redirect()
  101. elif self.path.startswith('/method'):
  102. self._method('POST', data)
  103. elif self.path.startswith('/headers'):
  104. self._headers()
  105. else:
  106. self._status(404)
  107. def do_HEAD(self):
  108. if self.path.startswith('/redirect_'):
  109. self._redirect()
  110. elif self.path.startswith('/method'):
  111. self._method('HEAD')
  112. else:
  113. self._status(404)
  114. def do_PUT(self):
  115. data = self._read_data()
  116. if self.path.startswith('/redirect_'):
  117. self._redirect()
  118. elif self.path.startswith('/method'):
  119. self._method('PUT', data)
  120. else:
  121. self._status(404)
  122. def do_GET(self):
  123. def respond(payload=b'<html><video src="/vid.mp4" /></html>',
  124. payload_type='text/html; charset=utf-8',
  125. payload_encoding=None,
  126. resp_code=200):
  127. self.send_response(resp_code)
  128. self.send_header('Content-Type', payload_type)
  129. if payload_encoding:
  130. self.send_header('Content-Encoding', payload_encoding)
  131. self.send_header('Content-Length', str(len(payload))) # required for persistent connections
  132. self.end_headers()
  133. self.wfile.write(payload)
  134. def gzip_compress(p):
  135. buf = io.BytesIO()
  136. with contextlib.closing(gzip.GzipFile(fileobj=buf, mode='wb')) as f:
  137. f.write(p)
  138. return buf.getvalue()
  139. if self.path == '/video.html':
  140. respond()
  141. elif self.path == '/vid.mp4':
  142. respond(b'\x00\x00\x00\x00\x20\x66\x74[video]', 'video/mp4')
  143. elif self.path == '/302':
  144. if sys.version_info[0] == 3:
  145. # XXX: Python 3 http server does not allow non-ASCII header values
  146. self.send_response(404)
  147. self.end_headers()
  148. return
  149. new_url = self._test_url('中文.html')
  150. self.send_response(302)
  151. self.send_header(b'Location', new_url.encode('utf-8'))
  152. self.end_headers()
  153. elif self.path == '/%E4%B8%AD%E6%96%87.html':
  154. respond()
  155. elif self.path == '/%c7%9f':
  156. respond()
  157. elif self.path == '/redirect_dotsegments':
  158. self.send_response(301)
  159. # redirect to /headers but with dot segments before
  160. self.send_header('Location', '/a/b/./../../headers')
  161. self.send_header('Content-Length', '0')
  162. self.end_headers()
  163. elif self.path.startswith('/redirect_'):
  164. self._redirect()
  165. elif self.path.startswith('/method'):
  166. self._method('GET')
  167. elif self.path.startswith('/headers'):
  168. self._headers()
  169. elif self.path.startswith('/308-to-headers'):
  170. self.send_response(308)
  171. self.send_header('Location', '/headers')
  172. self.send_header('Content-Length', '0')
  173. self.end_headers()
  174. elif self.path == '/trailing_garbage':
  175. payload = b'<html><video src="/vid.mp4" /></html>'
  176. compressed = gzip_compress(payload) + b'trailing garbage'
  177. respond(compressed, payload_encoding='gzip')
  178. elif self.path == '/302-non-ascii-redirect':
  179. new_url = self._test_url('中文.html')
  180. # actually respond with permanent redirect
  181. self.send_response(301)
  182. self.send_header('Location', new_url)
  183. self.send_header('Content-Length', '0')
  184. self.end_headers()
  185. elif self.path == '/content-encoding':
  186. encodings = self.headers.get('ytdl-encoding', '')
  187. payload = b'<html><video src="/vid.mp4" /></html>'
  188. for encoding in filter(None, (e.strip() for e in encodings.split(','))):
  189. if encoding == 'br' and brotli:
  190. payload = brotli.compress(payload)
  191. elif encoding == 'gzip':
  192. payload = gzip_compress(payload)
  193. elif encoding == 'deflate':
  194. payload = zlib.compress(payload)
  195. elif encoding == 'unsupported':
  196. payload = b'raw'
  197. break
  198. else:
  199. self._status(415)
  200. return
  201. respond(payload, payload_encoding=encodings)
  202. else:
  203. self._status(404)
  204. def send_header(self, keyword, value):
  205. """
  206. Forcibly allow HTTP server to send non percent-encoded non-ASCII characters in headers.
  207. This is against what is defined in RFC 3986: but we need to test that we support this
  208. since some sites incorrectly do this.
  209. """
  210. if keyword.lower() == 'connection':
  211. return self.super('send_header', keyword, value)
  212. if not hasattr(self, '_headers_buffer'):
  213. self._headers_buffer = []
  214. self._headers_buffer.append('{0}: {1}\r\n'.format(keyword, value).encode('utf-8'))
  215. def end_headers(self):
  216. if hasattr(self, '_headers_buffer'):
  217. self.wfile.write(b''.join(self._headers_buffer))
  218. self._headers_buffer = []
  219. self.super('end_headers')
  220. class TestHTTP(unittest.TestCase):
  221. # when does it make sense to check the SSL certificate?
  222. _check_cert = (
  223. sys.version_info >= (3, 2)
  224. or (sys.version_info[0] == 2 and sys.version_info[1:] >= (7, 19)))
  225. def setUp(self):
  226. # HTTP server
  227. self.http_httpd = compat_http_server.HTTPServer(
  228. ('127.0.0.1', 0), HTTPTestRequestHandler)
  229. self.http_port = http_server_port(self.http_httpd)
  230. self.http_server_thread = threading.Thread(target=self.http_httpd.serve_forever)
  231. self.http_server_thread.daemon = True
  232. self.http_server_thread.start()
  233. try:
  234. from http.server import ThreadingHTTPServer
  235. except ImportError:
  236. try:
  237. from socketserver import ThreadingMixIn
  238. except ImportError:
  239. from SocketServer import ThreadingMixIn
  240. class ThreadingHTTPServer(ThreadingMixIn, compat_http_server.HTTPServer):
  241. pass
  242. # HTTPS server
  243. certfn = os.path.join(TEST_DIR, 'testcert.pem')
  244. self.https_httpd = ThreadingHTTPServer(
  245. ('127.0.0.1', 0), HTTPTestRequestHandler)
  246. try:
  247. sslctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
  248. sslctx.verify_mode = ssl.CERT_NONE
  249. sslctx.check_hostname = False
  250. sslctx.load_cert_chain(certfn, None)
  251. self.https_httpd.socket = sslctx.wrap_socket(
  252. self.https_httpd.socket, server_side=True)
  253. except AttributeError:
  254. self.https_httpd.socket = ssl.wrap_socket(
  255. self.https_httpd.socket, certfile=certfn, server_side=True)
  256. self.https_port = http_server_port(self.https_httpd)
  257. self.https_server_thread = threading.Thread(target=self.https_httpd.serve_forever)
  258. self.https_server_thread.daemon = True
  259. self.https_server_thread.start()
  260. def tearDown(self):
  261. def closer(svr):
  262. def _closer():
  263. svr.shutdown()
  264. svr.server_close()
  265. return _closer
  266. shutdown_thread = threading.Thread(target=closer(self.http_httpd))
  267. shutdown_thread.start()
  268. self.http_server_thread.join(2.0)
  269. shutdown_thread = threading.Thread(target=closer(self.https_httpd))
  270. shutdown_thread.start()
  271. self.https_server_thread.join(2.0)
  272. def _test_url(self, path, host='127.0.0.1', scheme='http', port=None):
  273. return '{0}://{1}:{2}/{3}'.format(
  274. scheme, host,
  275. port if port is not None
  276. else self.https_port if scheme == 'https'
  277. else self.http_port, path)
  278. @unittest.skipUnless(_check_cert, 'No support for certificate check in SSL')
  279. def test_nocheckcertificate(self):
  280. with FakeYDL({'logger': FakeLogger()}) as ydl:
  281. with self.assertRaises(compat_urllib_error.URLError):
  282. ydl.urlopen(sanitized_Request(self._test_url('headers', scheme='https')))
  283. with FakeYDL({'logger': FakeLogger(), 'nocheckcertificate': True}) as ydl:
  284. r = ydl.urlopen(sanitized_Request(self._test_url('headers', scheme='https')))
  285. self.assertEqual(r.getcode(), 200)
  286. r.close()
  287. def test_percent_encode(self):
  288. with FakeYDL() as ydl:
  289. # Unicode characters should be encoded with uppercase percent-encoding
  290. res = ydl.urlopen(sanitized_Request(self._test_url('中文.html')))
  291. self.assertEqual(res.getcode(), 200)
  292. res.close()
  293. # don't normalize existing percent encodings
  294. res = ydl.urlopen(sanitized_Request(self._test_url('%c7%9f')))
  295. self.assertEqual(res.getcode(), 200)
  296. res.close()
  297. def test_unicode_path_redirection(self):
  298. with FakeYDL() as ydl:
  299. r = ydl.urlopen(sanitized_Request(self._test_url('302-non-ascii-redirect')))
  300. self.assertEqual(r.url, self._test_url('%E4%B8%AD%E6%96%87.html'))
  301. r.close()
  302. def test_redirect(self):
  303. with FakeYDL() as ydl:
  304. def do_req(redirect_status, method, check_no_content=False):
  305. data = b'testdata' if method in ('POST', 'PUT') else None
  306. res = ydl.urlopen(sanitized_Request(
  307. self._test_url('redirect_{0}'.format(redirect_status)),
  308. method=method, data=data))
  309. if check_no_content:
  310. self.assertNotIn('Content-Type', res.headers)
  311. return res.read().decode('utf-8'), res.headers.get('method', '')
  312. # A 303 must either use GET or HEAD for subsequent request
  313. self.assertEqual(do_req(303, 'POST'), ('', 'GET'))
  314. self.assertEqual(do_req(303, 'HEAD'), ('', 'HEAD'))
  315. self.assertEqual(do_req(303, 'PUT'), ('', 'GET'))
  316. # 301 and 302 turn POST only into a GET, with no Content-Type
  317. self.assertEqual(do_req(301, 'POST', True), ('', 'GET'))
  318. self.assertEqual(do_req(301, 'HEAD'), ('', 'HEAD'))
  319. self.assertEqual(do_req(302, 'POST', True), ('', 'GET'))
  320. self.assertEqual(do_req(302, 'HEAD'), ('', 'HEAD'))
  321. self.assertEqual(do_req(301, 'PUT'), ('testdata', 'PUT'))
  322. self.assertEqual(do_req(302, 'PUT'), ('testdata', 'PUT'))
  323. # 307 and 308 should not change method
  324. for m in ('POST', 'PUT'):
  325. self.assertEqual(do_req(307, m), ('testdata', m))
  326. self.assertEqual(do_req(308, m), ('testdata', m))
  327. self.assertEqual(do_req(307, 'HEAD'), ('', 'HEAD'))
  328. self.assertEqual(do_req(308, 'HEAD'), ('', 'HEAD'))
  329. # These should not redirect and instead raise an HTTPError
  330. for code in (300, 304, 305, 306):
  331. with self.assertRaises(compat_urllib_HTTPError):
  332. do_req(code, 'GET')
  333. # Jython 2.7.1 times out for some reason
  334. @expectedFailureIf(sys.platform.startswith('java') and sys.version_info < (2, 7, 2))
  335. def test_content_type(self):
  336. # https://github.com/yt-dlp/yt-dlp/commit/379a4f161d4ad3e40932dcf5aca6e6fb9715ab28
  337. with FakeYDL({'nocheckcertificate': True}) as ydl:
  338. # method should be auto-detected as POST
  339. r = sanitized_Request(self._test_url('headers', scheme='https'), data=urlencode_postdata({'test': 'test'}))
  340. headers = ydl.urlopen(r).read().decode('utf-8')
  341. self.assertIn('Content-Type: application/x-www-form-urlencoded', headers)
  342. # test http
  343. r = sanitized_Request(self._test_url('headers'), data=urlencode_postdata({'test': 'test'}))
  344. headers = ydl.urlopen(r).read().decode('utf-8')
  345. self.assertIn('Content-Type: application/x-www-form-urlencoded', headers)
  346. def test_update_req(self):
  347. req = sanitized_Request('http://example.com')
  348. assert req.data is None
  349. assert req.get_method() == 'GET'
  350. assert not req.has_header('Content-Type')
  351. # Test that zero-byte payloads will be sent
  352. req = update_Request(req, data=b'')
  353. assert req.data == b''
  354. assert req.get_method() == 'POST'
  355. # yt-dl expects data to be encoded and Content-Type to be added by sender
  356. # assert req.get_header('Content-Type') == 'application/x-www-form-urlencoded'
  357. def test_cookiejar(self):
  358. with FakeYDL() as ydl:
  359. ydl.cookiejar.set_cookie(compat_http_cookiejar_Cookie(
  360. 0, 'test', 'ytdl', None, False, '127.0.0.1', True,
  361. False, '/headers', True, False, None, False, None, None, {}))
  362. data = ydl.urlopen(sanitized_Request(
  363. self._test_url('headers'))).read().decode('utf-8')
  364. self.assertIn('Cookie: test=ytdl', data)
  365. def test_passed_cookie_header(self):
  366. # We should accept a Cookie header being passed as in normal headers and handle it appropriately.
  367. with FakeYDL() as ydl:
  368. # Specified Cookie header should be used
  369. res = ydl.urlopen(sanitized_Request(
  370. self._test_url('headers'), headers={'Cookie': 'test=test'})).read().decode('utf-8')
  371. self.assertIn('Cookie: test=test', res)
  372. # Specified Cookie header should be removed on any redirect
  373. res = ydl.urlopen(sanitized_Request(
  374. self._test_url('308-to-headers'), headers={'Cookie': 'test=test'})).read().decode('utf-8')
  375. self.assertNotIn('Cookie: test=test', res)
  376. # Specified Cookie header should override global cookiejar for that request
  377. ydl.cookiejar.set_cookie(compat_http_cookiejar_Cookie(
  378. 0, 'test', 'ytdlp', None, False, '127.0.0.1', True,
  379. False, '/headers', True, False, None, False, None, None, {}))
  380. data = ydl.urlopen(sanitized_Request(
  381. self._test_url('headers'), headers={'Cookie': 'test=test'})).read().decode('utf-8')
  382. self.assertNotIn('Cookie: test=ytdlp', data)
  383. self.assertIn('Cookie: test=test', data)
  384. def test_no_compression_compat_header(self):
  385. with FakeYDL() as ydl:
  386. data = ydl.urlopen(
  387. sanitized_Request(
  388. self._test_url('headers'),
  389. headers={'Youtubedl-no-compression': True})).read()
  390. self.assertIn(b'Accept-Encoding: identity', data)
  391. self.assertNotIn(b'youtubedl-no-compression', data.lower())
  392. def test_gzip_trailing_garbage(self):
  393. # https://github.com/ytdl-org/youtube-dl/commit/aa3e950764337ef9800c936f4de89b31c00dfcf5
  394. # https://github.com/ytdl-org/youtube-dl/commit/6f2ec15cee79d35dba065677cad9da7491ec6e6f
  395. with FakeYDL() as ydl:
  396. data = ydl.urlopen(sanitized_Request(self._test_url('trailing_garbage'))).read().decode('utf-8')
  397. self.assertEqual(data, '<html><video src="/vid.mp4" /></html>')
  398. def __test_compression(self, encoding):
  399. with FakeYDL() as ydl:
  400. res = ydl.urlopen(
  401. sanitized_Request(
  402. self._test_url('content-encoding'),
  403. headers={'ytdl-encoding': encoding}))
  404. # decoded encodings are removed: only check for valid decompressed data
  405. self.assertEqual(res.read(), b'<html><video src="/vid.mp4" /></html>')
  406. @unittest.skipUnless(brotli, 'brotli support is not installed')
  407. def test_brotli(self):
  408. self.__test_compression('br')
  409. def test_deflate(self):
  410. self.__test_compression('deflate')
  411. def test_gzip(self):
  412. self.__test_compression('gzip')
  413. def test_multiple_encodings(self):
  414. # https://www.rfc-editor.org/rfc/rfc9110.html#section-8.4
  415. for pair in ('gzip,deflate', 'deflate, gzip', 'gzip, gzip', 'deflate, deflate'):
  416. self.__test_compression(pair)
  417. def test_unsupported_encoding(self):
  418. # it should return the raw content
  419. with FakeYDL() as ydl:
  420. res = ydl.urlopen(
  421. sanitized_Request(
  422. self._test_url('content-encoding'),
  423. headers={'ytdl-encoding': 'unsupported'}))
  424. self.assertEqual(res.headers.get('Content-Encoding'), 'unsupported')
  425. self.assertEqual(res.read(), b'raw')
  426. def test_remove_dot_segments(self):
  427. with FakeYDL() as ydl:
  428. res = ydl.urlopen(sanitized_Request(self._test_url('a/b/./../../headers')))
  429. self.assertEqual(compat_urllib_parse.urlparse(res.geturl()).path, '/headers')
  430. res = ydl.urlopen(sanitized_Request(self._test_url('redirect_dotsegments')))
  431. self.assertEqual(compat_urllib_parse.urlparse(res.geturl()).path, '/headers')
  432. def _build_proxy_handler(name):
  433. class HTTPTestRequestHandler(compat_http_server.BaseHTTPRequestHandler):
  434. proxy_name = name
  435. def log_message(self, format, *args):
  436. pass
  437. def do_GET(self):
  438. self.send_response(200)
  439. self.send_header('Content-Type', 'text/plain; charset=utf-8')
  440. self.end_headers()
  441. self.wfile.write('{0}: {1}'.format(self.proxy_name, self.path).encode('utf-8'))
  442. return HTTPTestRequestHandler
  443. class TestProxy(unittest.TestCase):
  444. def setUp(self):
  445. self.proxy = compat_http_server.HTTPServer(
  446. ('127.0.0.1', 0), _build_proxy_handler('normal'))
  447. self.port = http_server_port(self.proxy)
  448. self.proxy_thread = threading.Thread(target=self.proxy.serve_forever)
  449. self.proxy_thread.daemon = True
  450. self.proxy_thread.start()
  451. self.geo_proxy = compat_http_server.HTTPServer(
  452. ('127.0.0.1', 0), _build_proxy_handler('geo'))
  453. self.geo_port = http_server_port(self.geo_proxy)
  454. self.geo_proxy_thread = threading.Thread(target=self.geo_proxy.serve_forever)
  455. self.geo_proxy_thread.daemon = True
  456. self.geo_proxy_thread.start()
  457. def tearDown(self):
  458. def closer(svr):
  459. def _closer():
  460. svr.shutdown()
  461. svr.server_close()
  462. return _closer
  463. shutdown_thread = threading.Thread(target=closer(self.proxy))
  464. shutdown_thread.start()
  465. self.proxy_thread.join(2.0)
  466. shutdown_thread = threading.Thread(target=closer(self.geo_proxy))
  467. shutdown_thread.start()
  468. self.geo_proxy_thread.join(2.0)
  469. def _test_proxy(self, host='127.0.0.1', port=None):
  470. return '{0}:{1}'.format(
  471. host, port if port is not None else self.port)
  472. def test_proxy(self):
  473. geo_proxy = self._test_proxy(port=self.geo_port)
  474. ydl = YoutubeDL({
  475. 'proxy': self._test_proxy(),
  476. 'geo_verification_proxy': geo_proxy,
  477. })
  478. url = 'http://foo.com/bar'
  479. response = ydl.urlopen(url).read().decode('utf-8')
  480. self.assertEqual(response, 'normal: {0}'.format(url))
  481. req = compat_urllib_request.Request(url)
  482. req.add_header('Ytdl-request-proxy', geo_proxy)
  483. response = ydl.urlopen(req).read().decode('utf-8')
  484. self.assertEqual(response, 'geo: {0}'.format(url))
  485. def test_proxy_with_idn(self):
  486. ydl = YoutubeDL({
  487. 'proxy': self._test_proxy(),
  488. })
  489. url = 'http://中文.tw/'
  490. response = ydl.urlopen(url).read().decode('utf-8')
  491. # b'xn--fiq228c' is '中文'.encode('idna')
  492. self.assertEqual(response, 'normal: http://xn--fiq228c.tw/')
  493. class TestFileURL(unittest.TestCase):
  494. # See https://github.com/ytdl-org/youtube-dl/issues/8227
  495. def test_file_urls(self):
  496. tf = tempfile.NamedTemporaryFile(delete=False)
  497. tf.write(b'foobar')
  498. tf.close()
  499. url = compat_urllib_parse.urljoin('file://', pathname2url(tf.name))
  500. with FakeYDL() as ydl:
  501. self.assertRaisesRegexp(
  502. compat_urllib_error.URLError, 'file:// scheme is explicitly disabled in youtube-dl for security reasons', ydl.urlopen, url)
  503. # not yet implemented
  504. """
  505. with FakeYDL({'enable_file_urls': True}) as ydl:
  506. res = ydl.urlopen(url)
  507. self.assertEqual(res.read(), b'foobar')
  508. res.close()
  509. """
  510. os.unlink(tf.name)
  511. if __name__ == '__main__':
  512. unittest.main()