Skip to content
This repository was archived by the owner on Mar 15, 2018. It is now read-only.

Commit cabf11a

Browse files
committed
port manifest-fetching code to use requests instead of urllib2 (bug 941480)
1 parent 698fbbf commit cabf11a

File tree

2 files changed

+80
-71
lines changed

2 files changed

+80
-71
lines changed

mkt/developers/tasks.py

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@
44
import logging
55
import os
66
import sys
7-
import time
87
import traceback
9-
import urllib2
108
import urlparse
119
import uuid
1210
import zipfile
11+
from datetime import date
1312

1413
from django import forms
1514
from django.conf import settings
1615
from django.core.files.storage import default_storage as storage
17-
from django.utils import translation
1816
from django.utils.http import urlencode
1917

18+
import requests
2019
from appvalidator import validate_app, validate_packaged_app
2120
from celery_tasktree import task_with_callbacks
2221
from celeryutils import task
@@ -28,18 +27,20 @@
2827
from addons.models import Addon
2928
from amo.decorators import set_modified_on, write
3029
from amo.helpers import absolutify
31-
from amo.utils import (remove_icons, resize_image, send_mail_jinja, strip_bom,
32-
to_language)
30+
from amo.utils import remove_icons, resize_image, send_mail_jinja, strip_bom
3331
from files.models import FileUpload, File, FileValidation
3432
from files.utils import SafeUnzip
3533

3634
from mkt.constants import APP_PREVIEW_SIZES
37-
from mkt.constants.regions import REGIONS_CHOICES_SLUG
3835
from mkt.webapps.models import AddonExcludedRegion, Webapp
3936

4037

4138
log = logging.getLogger('z.mkt.developers.task')
4239

40+
CT_URL = (
41+
'https://developer.mozilla.org/docs/Web/Apps/Manifest#Serving_manifests'
42+
)
43+
4344

4445
@task
4546
@write
@@ -205,19 +206,21 @@ def get_preview_sizes(ids, **kw):
205206
def _fetch_content(url):
206207
with statsd.timer('developers.tasks.fetch_content'):
207208
try:
208-
stream = urllib2.urlopen(url, timeout=30)
209-
if not 200 <= stream.getcode() < 300:
210-
raise Exception(
211-
'An invalid HTTP status code was returned.')
212-
if not stream.headers.keys():
213-
raise Exception(
214-
'The HTTP server did not return headers.')
215-
return stream
216-
except urllib2.HTTPError, e:
217-
raise Exception(
218-
'%s responded with %s (%s).' % (url, e.code, e.msg))
219-
except urllib2.URLError, e:
220-
# Unpack the URLError to try and find a useful message.
209+
res = requests.get(url, timeout=30, stream=True)
210+
211+
if not 200 <= res.status_code < 300:
212+
statsd.incr('developers.tasks.fetch_content.error')
213+
raise Exception('An invalid HTTP status code was returned.')
214+
215+
if not res.headers.keys():
216+
statsd.incr('developers.tasks.fetch_content.error')
217+
raise Exception('The HTTP server did not return headers.')
218+
219+
statsd.incr('developers.tasks.fetch_content.success')
220+
return res
221+
except requests.RequestException as e:
222+
statsd.incr('developers.tasks.fetch_content.error')
223+
log.error('fetch_content connection error: %s' % e)
221224
raise Exception('The file could not be retrieved.')
222225

223226

@@ -228,7 +231,8 @@ class ResponseTooLargeException(Exception):
228231
def get_content_and_check_size(response, max_size):
229232
# Read one extra byte. Reject if it's too big so we don't have issues
230233
# downloading huge files.
231-
content = response.read(max_size + 1)
234+
content = response.iter_content(chunk_size=max_size + 1,
235+
decode_unicode=True).next()
232236
if len(content) > max_size:
233237
raise ResponseTooLargeException('Too much data.')
234238
return content
@@ -330,8 +334,6 @@ def failed_validation(*messages, **kwargs):
330334
'prelim': True})
331335

332336

333-
CT_URL = (
334-
'https://developer.mozilla.org/docs/Web/Apps/Manifest#Serving_manifests')
335337
def _fetch_manifest(url, upload=None):
336338
def fail(message, upload=None):
337339
if upload is None:
@@ -348,7 +350,7 @@ def fail(message, upload=None):
348350
'again.'), upload=upload)
349351
return
350352

351-
ct = response.headers.get('Content-Type', '')
353+
ct = response.headers.get('content-type', '')
352354
if not ct.startswith('application/x-web-app-manifest+json'):
353355
fail(_('Manifests must be served with the HTTP header '
354356
'"Content-Type: application/x-web-app-manifest+json". See %s '
@@ -416,17 +418,17 @@ def subscribe_to_responsys(campaign, address, format='html', source_url='',
416418
'SOURCE_URL': source_url,
417419
'EMAIL_ADDRESS_': address,
418420
'EMAIL_FORMAT_': 'H' if format == 'html' else 'T',
419-
}
421+
}
420422

421423
data['%s_FLG' % campaign] = 'Y'
422424
data['%s_DATE' % campaign] = date.today().strftime('%Y-%m-%d')
423425
data['_ri_'] = settings.RESPONSYS_ID
424426

425427
try:
426-
res = urllib2.urlopen('http://awesomeness.mozilla.org/pub/rf',
427-
data=urlencode(data))
428-
return res.code == 200
429-
except urllib2.URLError:
428+
res = requests.get('http://awesomeness.mozilla.org/pub/rf',
429+
data=urlencode(data))
430+
return res.status_code == 200
431+
except requests.RequestException:
430432
return False
431433

432434

@@ -457,8 +459,9 @@ def region_email(ids, regions, **kw):
457459
to = set(product.authors.values_list('email', flat=True))
458460

459461
if len(regions) == 1:
460-
subject = _(u'{region} region added to the Firefox Marketplace'
461-
).format(region=regions[0])
462+
subject = _(
463+
u'{region} region added to the Firefox Marketplace').format(
464+
region=regions[0])
462465
else:
463466
subject = _(u'New regions added to the Firefox Marketplace')
464467

mkt/developers/tests/test_tasks.py

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import codecs
2-
from contextlib import contextmanager
3-
from cStringIO import StringIO
42
import json
53
import os
64
import shutil
75
import socket
86
import tempfile
97
import urllib2
8+
from contextlib import contextmanager
9+
from cStringIO import StringIO
1010

1111
from django.conf import settings
1212
from django.core import mail
@@ -15,6 +15,7 @@
1515
import mock
1616
from nose.tools import eq_
1717
from PIL import Image
18+
from requests import RequestException
1819

1920
import amo
2021
import amo.tests
@@ -168,6 +169,8 @@ def test_validate_packaged_app(self, _zipfile, _mock):
168169

169170

170171
storage_open = storage.open
172+
173+
171174
def _mock_hide_64px_icon(path, *args, **kwargs):
172175
"""
173176
A function that mocks `storage.open` and throws an IOError if you try to
@@ -214,8 +217,8 @@ def setUp(self):
214217
self.upload = FileUpload.objects.create()
215218
self.content_type = 'application/x-web-app-manifest+json'
216219

217-
patcher = mock.patch('mkt.developers.tasks.urllib2.urlopen')
218-
self.urlopen_mock = patcher.start()
220+
patcher = mock.patch('mkt.developers.tasks.requests.get')
221+
self.requests_mock = patcher.start()
219222
self.addCleanup(patcher.stop)
220223

221224
def get_upload(self):
@@ -225,19 +228,18 @@ def file(self, name):
225228
return os.path.join(os.path.dirname(__file__), 'addons', name)
226229

227230
@contextmanager
228-
def patch_urlopen(self):
229-
response_mock = mock.Mock()
230-
response_mock.getcode.return_value = 200
231-
response_mock.read.return_value = '<default>'
232-
response_mock.headers = {'Content-Type': self.content_type}
231+
def patch_requests(self):
232+
response_mock = mock.Mock(status_code=200)
233+
response_mock.iter_content.return_value = mock.Mock(
234+
next=lambda: '<default>')
235+
response_mock.headers = {'content-type': self.content_type}
233236
yield response_mock
234-
self.urlopen_mock.return_value = response_mock
237+
self.requests_mock.return_value = response_mock
235238

236239
@mock.patch('mkt.developers.tasks.validator')
237240
def test_success_add_file(self, validator_mock):
238-
with self.patch_urlopen() as ur:
239-
ur.read.return_value = 'woo'
240-
ur.headers = {'Content-Type': self.content_type}
241+
with self.patch_requests() as ur:
242+
ur.iter_content.return_value = mock.Mock(next=lambda: 'woo')
241243

242244
tasks.fetch_manifest('http://xx.com/manifest.json', self.upload.pk)
243245
upload = FileUpload.objects.get(pk=self.upload.pk)
@@ -247,9 +249,9 @@ def test_success_add_file(self, validator_mock):
247249

248250
@mock.patch('mkt.developers.tasks.validator')
249251
def test_success_call_validator(self, validator_mock):
250-
with self.patch_urlopen() as ur:
252+
with self.patch_requests() as ur:
251253
ct = self.content_type + '; charset=utf-8'
252-
ur.headers = {'Content-Type': ct}
254+
ur.headers = {'content-type': ct}
253255

254256
tasks.fetch_manifest('http://xx.com/manifest.json', self.upload.pk)
255257
assert validator_mock.called
@@ -273,31 +275,31 @@ def check_validation(self, msg=''):
273275

274276
def test_connection_error(self):
275277
reason = socket.gaierror(8, 'nodename nor servname provided')
276-
self.urlopen_mock.side_effect = urllib2.URLError(reason)
278+
self.requests_mock.side_effect = RequestException(reason)
277279
tasks.fetch_manifest('url', self.upload.pk)
278280
self.check_validation(
279281
'No manifest was found at that URL. Check the address and try '
280282
'again.')
281283

282284
def test_url_timeout(self):
283285
reason = socket.timeout('too slow')
284-
self.urlopen_mock.side_effect = urllib2.URLError(reason)
286+
self.requests_mock.side_effect = RequestException(reason)
285287
tasks.fetch_manifest('url', self.upload.pk)
286288
self.check_validation(
287289
'No manifest was found at that URL. Check the address and try '
288290
'again.')
289291

290292
def test_other_url_error(self):
291293
reason = Exception('Some other failure.')
292-
self.urlopen_mock.side_effect = urllib2.URLError(reason)
294+
self.requests_mock.side_effect = RequestException(reason)
293295
tasks.fetch_manifest('url', self.upload.pk)
294296
self.check_validation(
295297
'No manifest was found at that URL. Check the address and try '
296298
'again.')
297299

298300
@mock.patch('mkt.developers.tasks.validator', lambda uid, **kw: None)
299301
def test_no_content_type(self):
300-
with self.patch_urlopen() as ur:
302+
with self.patch_requests() as ur:
301303
ur.headers = {}
302304

303305
tasks.fetch_manifest('url', self.upload.pk)
@@ -307,7 +309,7 @@ def test_no_content_type(self):
307309

308310
@mock.patch('mkt.developers.tasks.validator', lambda uid, **kw: None)
309311
def test_bad_content_type(self):
310-
with self.patch_urlopen() as ur:
312+
with self.patch_requests() as ur:
311313
ur.headers = {'Content-Type': 'x'}
312314

313315
tasks.fetch_manifest('url', self.upload.pk)
@@ -318,63 +320,67 @@ def test_bad_content_type(self):
318320

319321
@mock.patch('mkt.developers.tasks.validator', lambda uid, **kw: None)
320322
def test_good_charset(self):
321-
with self.patch_urlopen() as ur:
323+
with self.patch_requests() as ur:
322324
ur.headers = {
323-
'Content-Type': 'application/x-web-app-manifest+json;'
324-
'charset=utf-8'}
325+
'content-type': 'application/x-web-app-manifest+json;'
326+
'charset=utf-8'
327+
}
325328

326329
tasks.fetch_manifest('url', self.upload.pk)
327330
self.check_validation()
328331

329332
@mock.patch('mkt.developers.tasks.validator', lambda uid, **kw: None)
330333
def test_bad_charset(self):
331-
with self.patch_urlopen() as ur:
334+
with self.patch_requests() as ur:
332335
ur.headers = {
333-
'Content-Type': 'application/x-web-app-manifest+json;'
334-
'charset=ISO-1234567890-LOL'}
336+
'content-type': 'application/x-web-app-manifest+json;'
337+
'charset=ISO-1234567890-LOL'
338+
}
335339

336340
tasks.fetch_manifest('url', self.upload.pk)
337341
self.check_validation("The manifest's encoding does not match the "
338342
'charset provided in the HTTP Content-Type.')
339343

340344
def test_response_too_large(self):
341-
with self.patch_urlopen() as ur:
345+
with self.patch_requests() as ur:
342346
content = 'x' * (settings.MAX_WEBAPP_UPLOAD_SIZE + 1)
343-
ur.read.return_value = content
347+
ur.iter_content.return_value = mock.Mock(next=lambda: content)
344348

345349
tasks.fetch_manifest('url', self.upload.pk)
346350
max_webapp_size = settings.MAX_WEBAPP_UPLOAD_SIZE
347351
self.check_validation('Your manifest must be less than %s bytes.' %
348352
max_webapp_size)
349353

350354
def test_http_error(self):
351-
self.urlopen_mock.side_effect = urllib2.HTTPError(
355+
self.requests_mock.side_effect = urllib2.HTTPError(
352356
'url', 404, 'Not Found', [], None)
353357
tasks.fetch_manifest('url', self.upload.pk)
354358
self.check_validation(
355359
'No manifest was found at that URL. Check the address and try '
356360
'again.')
357361

358362
def test_strip_utf8_bom(self):
359-
with self.patch_urlopen() as ur:
363+
with self.patch_requests() as ur:
360364
with open(self.file('utf8bom.webapp')) as fp:
361-
ur.read.return_value = fp.read()
365+
content = fp.read()
366+
ur.iter_content.return_value = mock.Mock(next=lambda: content)
362367

363368
tasks.fetch_manifest('url', self.upload.pk)
364369
upload = self.get_upload()
365370
with storage.open(upload.path, 'rb') as fp:
366371
manifest = fp.read()
367-
json.loads(manifest) # no parse error
372+
json.loads(manifest) # No parse error.
368373
assert not manifest.startswith(codecs.BOM_UTF8)
369374

370375
def test_non_utf8_encoding(self):
371-
with self.patch_urlopen() as ur:
376+
with self.patch_requests() as ur:
372377
with open(self.file('utf8bom.webapp')) as fp:
373-
# Set encoding to utf16 which will be invalid
374-
ur.read.return_value = fp.read().decode('utf8').encode('utf16')
378+
# Set encoding to utf16 which will be invalid.
379+
content = fp.read().decode('utf8').encode('utf16')
380+
ur.iter_content.return_value = mock.Mock(next=lambda: content)
375381
tasks.fetch_manifest('url', self.upload.pk)
376382
self.check_validation(
377-
'Your manifest file was not encoded as valid UTF-8.')
383+
'Your manifest file was not encoded as valid UTF-8.')
378384

379385

380386
class TestFetchIcon(BaseWebAppTest):
@@ -384,9 +390,9 @@ def setUp(self):
384390
self.content_type = 'image/png'
385391
self.apps_path = os.path.join(settings.ROOT, 'apps', 'devhub', 'tests',
386392
'addons')
387-
patcher = mock.patch('mkt.developers.tasks.urllib2.urlopen')
388-
self.urlopen_mock = patcher.start()
389-
self.urlopen_mock.return_value = StringIO('mozballin')
393+
patcher = mock.patch('mkt.developers.tasks.requests.get')
394+
self.requests_mock = patcher.start()
395+
self.requests_mock.return_value = StringIO('mozballin')
390396
self.addCleanup(patcher.stop)
391397

392398
def webapp_from_path(self, path):
@@ -404,13 +410,13 @@ def test_no_icons(self):
404410
path = os.path.join(self.apps_path, 'noicon.webapp')
405411
iconless_app = self.webapp_from_path(path)
406412
tasks.fetch_icon(iconless_app)
407-
assert not self.urlopen_mock.called
413+
assert not self.requests_mock.called
408414

409415
def test_bad_icons(self):
410416
path = os.path.join(self.apps_path, 'badicon.webapp')
411417
iconless_app = self.webapp_from_path(path)
412418
tasks.fetch_icon(iconless_app)
413-
assert not self.urlopen_mock.called
419+
assert not self.requests_mock.called
414420

415421
def check_icons(self, webapp):
416422
manifest = webapp.get_manifest_json()

0 commit comments

Comments
 (0)