Serve JSON update manifests instead of RDF, but to Firefox only (#8271)

Serve JSON update manifests instead of RDF, but to Firefox only
This commit is contained in:
Mathieu Pillard 2018-05-17 14:39:46 +02:00 коммит произвёл GitHub
Родитель 968c7636bf
Коммит 131c8f22db
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 302 добавлений и 267 удалений

Просмотреть файл

@ -1,3 +1,4 @@
import json
import sys import sys
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
@ -18,8 +19,7 @@ except ImportError:
from olympia.constants import applications, base from olympia.constants import applications, base
import olympia.core.logger import olympia.core.logger
from utils import ( from utils import get_cdn_url, log_configure, PLATFORM_NAMES_TO_CONSTANTS
APP_GUIDS, get_cdn_url, log_configure, PLATFORMS)
# Go configure the log. # Go configure the log.
@ -81,6 +81,15 @@ class Update(object):
self.data['row'] = {} self.data['row'] = {}
self.version_int = 0 self.version_int = 0
self.compat_mode = compat_mode self.compat_mode = compat_mode
self.use_json = self.should_use_json()
def should_use_json(self):
# We serve JSON manifests to Firefox and Firefox for Android only,
# because we've seen issues with Seamonkey and Thunderbird.
# https://github.com/mozilla/addons-server/issues/7223
app = applications.APP_GUIDS.get(self.data.get('appID'))
return app and app.id in (applications.FIREFOX.id,
applications.ANDROID.id)
def is_valid(self): def is_valid(self):
# If you accessing this from unit tests, then before calling # If you accessing this from unit tests, then before calling
@ -96,10 +105,12 @@ class Update(object):
if field not in data: if field not in data:
return False return False
data['app_id'] = APP_GUIDS.get(data['appID']) app = applications.APP_GUIDS.get(data['appID'])
if not data['app_id']: if not app:
return False return False
data['app_id'] = app.id
sql = """SELECT id, status, addontype_id, guid FROM addons sql = """SELECT id, status, addontype_id, guid FROM addons
WHERE guid = %(guid)s AND WHERE guid = %(guid)s AND
inactive = 0 AND inactive = 0 AND
@ -118,7 +129,7 @@ class Update(object):
data['version_int'] = version_int(data['appVersion']) data['version_int'] = version_int(data['appVersion'])
if 'appOS' in data: if 'appOS' in data:
for k, v in PLATFORMS.items(): for k, v in PLATFORM_NAMES_TO_CONSTANTS.items():
if k in data['appOS']: if k in data['appOS']:
data['appOS'] = v data['appOS'] = v
break break
@ -243,27 +254,68 @@ class Update(object):
return False return False
def get_bad_rdf(self): def get_output(self):
return bad_rdf
def get_rdf(self):
if self.is_valid(): if self.is_valid():
if self.get_update(): if self.get_update():
rdf = self.get_good_rdf() contents = self.get_success_output()
else: else:
rdf = self.get_no_updates_rdf() contents = self.get_no_updates_output()
else: else:
rdf = self.get_bad_rdf() contents = self.get_error_output()
self.cursor.close() self.cursor.close()
if self.conn: if self.conn:
self.conn.close() self.conn.close()
return rdf return json.dumps(contents) if self.use_json else contents
def get_no_updates_rdf(self): def get_error_output(self):
name = base.ADDON_SLUGS_UPDATE[self.data['type']] return {} if self.use_json else bad_rdf
return no_updates_rdf % ({'guid': self.data['guid'], 'type': name})
def get_good_rdf(self): def get_no_updates_output(self):
if self.use_json:
return {
'addons': {
self.data['guid']: {
'updates': []
}
}
}
else:
name = base.ADDON_SLUGS_UPDATE[self.data['type']]
return no_updates_rdf % ({'guid': self.data['guid'], 'type': name})
def get_success_output(self):
if self.use_json:
return self.get_success_output_json()
else:
return self.get_success_output_rdf()
def get_success_output_json(self):
data = self.data['row']
update = {
'version': data['version'],
'update_link': data['url'],
'applications': {
'gecko': {
'strict_min_version': data['min']
}
}
}
if data['strict_compat']:
update['applications']['gecko']['strict_max_version'] = data['max']
if data['hash']:
update['update_hash'] = data['hash']
if data['releasenotes']:
update['update_info_url'] = '%s%s%s/%%APP_LOCALE%%/' % (
settings.SITE_URL, '/versions/updateInfo/', data['version_id'])
return {
'addons': {
self.data['guid']: {
'updates': [update]
}
}
}
def get_success_output_rdf(self):
data = self.data['row'] data = self.data['row']
data['if_hash'] = '' data['if_hash'] = ''
if data['hash']: if data['hash']:
@ -283,7 +335,8 @@ class Update(object):
return '%s GMT' % formatdate(time() + secs)[:25] return '%s GMT' % formatdate(time() + secs)[:25]
def get_headers(self, length): def get_headers(self, length):
return [('Content-Type', 'text/xml'), content_type = 'application/json' if self.use_json else 'text/xml'
return [('Content-Type', content_type),
('Cache-Control', 'public, max-age=3600'), ('Cache-Control', 'public, max-age=3600'),
('Last-Modified', self.format_date(0)), ('Last-Modified', self.format_date(0)),
('Expires', self.format_date(3600)), ('Expires', self.format_date(3600)),
@ -302,7 +355,7 @@ def application(environ, start_response):
compat_mode = data.pop('compatMode', 'strict') compat_mode = data.pop('compatMode', 'strict')
try: try:
update = Update(data, compat_mode) update = Update(data, compat_mode)
output = force_bytes(update.get_rdf()) output = force_bytes(update.get_output())
start_response(status, update.get_headers(len(output))) start_response(status, update.get_headers(len(output)))
except Exception: except Exception:
log_exception(data) log_exception(data)

Просмотреть файл

@ -17,7 +17,6 @@ from olympia.lib.log_settings_base import formatters, handlers
# Ugh. But this avoids any olympia models or django imports at all. # Ugh. But this avoids any olympia models or django imports at all.
# Perhaps we can import these without any problems and we can # Perhaps we can import these without any problems and we can
# remove all this. # remove all this.
from olympia.constants.applications import APPS_ALL
from olympia.constants.platforms import PLATFORMS from olympia.constants.platforms import PLATFORMS
@ -51,8 +50,9 @@ def user_media_url(what):
return getattr(settings, key, default) return getattr(settings, key, default)
APP_GUIDS = dict([(app.guid, app.id) for app in APPS_ALL.values()]) PLATFORM_NAMES_TO_CONSTANTS = {
PLATFORMS = dict([(plat.api_name, plat.id) for plat in PLATFORMS.values()]) platform.api_name: platform.id for platform in PLATFORMS.values()
}
ADDON_SLUGS_UPDATE = { ADDON_SLUGS_UPDATE = {
1: 'extension', 1: 'extension',

Просмотреть файл

@ -1,7 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json
import mock
from datetime import datetime, timedelta from datetime import datetime, timedelta
from email import utils from email import utils
import rdflib
from django.db import connection from django.db import connection
from services import update from services import update
@ -17,10 +22,10 @@ from olympia.versions.models import ApplicationsVersions, Version
class VersionCheckMixin(object): class VersionCheckMixin(object):
def get(self, data): def get_update_instance(self, data):
up = update.Update(data) instance = update.Update(data)
up.cursor = connection.cursor() instance.cursor = connection.cursor()
return up return instance
class TestDataValidate(VersionCheckMixin, TestCase): class TestDataValidate(VersionCheckMixin, TestCase):
@ -28,7 +33,7 @@ class TestDataValidate(VersionCheckMixin, TestCase):
def setUp(self): def setUp(self):
super(TestDataValidate, self).setUp() super(TestDataValidate, self).setUp()
self.good_data = { self.data = {
'id': '{2fa4ed95-0317-4c6a-a74c-5f3e3912c1f9}', 'id': '{2fa4ed95-0317-4c6a-a74c-5f3e3912c1f9}',
'version': '2.0.58', 'version': '2.0.58',
'reqVersion': 1, 'reqVersion': 1,
@ -37,74 +42,74 @@ class TestDataValidate(VersionCheckMixin, TestCase):
} }
def test_app_os(self): def test_app_os(self):
data = self.good_data.copy() data = self.data.copy()
data['appOS'] = 'something %s penguin' % amo.PLATFORM_LINUX.api_name data['appOS'] = 'something %s penguin' % amo.PLATFORM_LINUX.api_name
form = self.get(data) instance = self.get_update_instance(data)
assert form.is_valid() assert instance.is_valid()
assert form.data['appOS'] == amo.PLATFORM_LINUX.id assert instance.data['appOS'] == amo.PLATFORM_LINUX.id
def test_app_version_fails(self): def test_app_version_fails(self):
data = self.good_data.copy() data = self.data.copy()
del data['appID'] del data['appID']
form = self.get(data) instance = self.get_update_instance(data)
assert not form.is_valid() assert not instance.is_valid()
def test_app_version_wrong(self): def test_app_version_wrong(self):
data = self.good_data.copy() data = self.data.copy()
data['appVersion'] = '67.7' data['appVersion'] = '67.7'
form = self.get(data) instance = self.get_update_instance(data)
# If you pass through the wrong version that's fine # If you pass through the wrong version that's fine
# you will just end up with no updates because your # you will just end up with no updates because your
# version_int will be out. # version_int will be out.
assert form.is_valid() assert instance.is_valid()
def test_app_version(self): def test_app_version(self):
data = self.good_data.copy() data = self.data.copy()
form = self.get(data) instance = self.get_update_instance(data)
assert form.is_valid() assert instance.is_valid()
assert form.data['version_int'] == 3070000001000 assert instance.data['version_int'] == 3070000001000
def test_sql_injection(self): def test_sql_injection(self):
data = self.good_data.copy() data = self.data.copy()
data['id'] = "'" data['id'] = "'"
up = self.get(data) instance = self.get_update_instance(data)
assert not up.is_valid() assert not instance.is_valid()
def test_inactive(self): def test_inactive(self):
addon = Addon.objects.get(pk=3615) addon = Addon.objects.get(pk=3615)
addon.update(disabled_by_user=True) addon.update(disabled_by_user=True)
up = self.get(self.good_data) instance = self.get_update_instance(self.data)
assert not up.is_valid() assert not instance.is_valid()
def test_soft_deleted(self): def test_soft_deleted(self):
addon = Addon.objects.get(pk=3615) addon = Addon.objects.get(pk=3615)
addon.update(status=amo.STATUS_DELETED) addon.update(status=amo.STATUS_DELETED)
up = self.get(self.good_data) instance = self.get_update_instance(self.data)
assert not up.is_valid() assert not instance.is_valid()
def test_disabled(self): def test_disabled(self):
addon = Addon.objects.get(pk=3615) addon = Addon.objects.get(pk=3615)
addon.update(status=amo.STATUS_DISABLED) addon.update(status=amo.STATUS_DISABLED)
up = self.get(self.good_data) instance = self.get_update_instance(self.data)
assert not up.is_valid() assert not instance.is_valid()
def test_no_version(self): def test_no_version(self):
data = self.good_data.copy() data = self.data.copy()
del data['version'] del data['version']
up = self.get(data) instance = self.get_update_instance(data)
assert up.is_valid() assert instance.is_valid()
def test_unlisted_addon(self): def test_unlisted_addon(self):
"""Add-ons with only unlisted versions are valid, they just don't """Add-ons with only unlisted versions are valid, they just don't
receive any updates (See TestLookup.test_no_unlisted below).""" receive any updates (See TestLookinstance.test_no_unlisted below)."""
addon = Addon.objects.get(pk=3615) addon = Addon.objects.get(pk=3615)
self.make_addon_unlisted(addon) self.make_addon_unlisted(addon)
up = self.get(self.good_data) instance = self.get_update_instance(self.data)
assert up.is_valid() assert instance.is_valid()
class TestLookup(VersionCheckMixin, TestCase): class TestLookup(VersionCheckMixin, TestCase):
@ -123,7 +128,7 @@ class TestLookup(VersionCheckMixin, TestCase):
self.version_1_2_1 = 112396 self.version_1_2_1 = 112396
self.version_1_2_2 = 115509 self.version_1_2_2 = 115509
def get(self, *args): def get_update_instance(self, *args):
data = { data = {
'id': self.addon.guid, 'id': self.addon.guid,
'appID': args[2].guid, 'appID': args[2].guid,
@ -134,12 +139,12 @@ class TestLookup(VersionCheckMixin, TestCase):
# Allow version to be optional. # Allow version to be optional.
if args[0]: if args[0]:
data['version'] = args[0] data['version'] = args[0]
up = super(TestLookup, self).get(data) instance = super(TestLookup, self).get_update_instance(data)
assert up.is_valid() assert instance.is_valid()
up.data['version_int'] = args[1] instance.data['version_int'] = args[1]
up.get_update() instance.get_update()
return (up.data['row'].get('version_id'), return (instance.data['row'].get('version_id'),
up.data['row'].get('file_id')) instance.data['row'].get('file_id'))
def change_status(self, version, status): def change_status(self, version, status):
version = Version.objects.get(pk=version) version = Version.objects.get(pk=version)
@ -156,8 +161,8 @@ class TestLookup(VersionCheckMixin, TestCase):
Version 3.0a1 of Firefox is 3000000001100 and version 1.0.2 of the Version 3.0a1 of Firefox is 3000000001100 and version 1.0.2 of the
add-on is returned. add-on is returned.
""" """
version, file = self.get('', '3000000001100', version, file = self.get_update_instance(
self.app, self.platform) '', '3000000001100', self.app, self.platform)
assert version == self.version_1_0_2 assert version == self.version_1_0_2
def test_new_client(self): def test_new_client(self):
@ -165,8 +170,8 @@ class TestLookup(VersionCheckMixin, TestCase):
Version 3.0.12 of Firefox is 3069900200100 and version 1.2.2 of the Version 3.0.12 of Firefox is 3069900200100 and version 1.2.2 of the
add-on is returned. add-on is returned.
""" """
version, file = self.get('', self.version_int, version, file = self.get_update_instance(
self.app, self.platform) '', self.version_int, self.app, self.platform)
assert version == self.version_1_2_2 assert version == self.version_1_2_2
def test_min_client(self): def test_min_client(self):
@ -180,8 +185,8 @@ class TestLookup(VersionCheckMixin, TestCase):
appversion.min = AppVersion.objects.get(pk=325) # 3.7a5 appversion.min = AppVersion.objects.get(pk=325) # 3.7a5
appversion.save() appversion.save()
version, file = self.get('', '3070000005000', # 3.7a5pre version, file = self.get_update_instance(
self.app, self.platform) '', '3070000005000', self.app, self.platform) # 3.7a5pre
assert version == self.version_1_1_3 assert version == self.version_1_1_3
def test_new_client_ordering(self): def test_new_client_ordering(self):
@ -201,8 +206,8 @@ class TestLookup(VersionCheckMixin, TestCase):
application_version.max_id = 329 application_version.max_id = 329
application_version.save() application_version.save()
version, file = self.get('', self.version_int, version, file = self.get_update_instance(
self.app, self.platform) '', self.version_int, self.app, self.platform)
assert version == self.version_1_2_2 assert version == self.version_1_2_2
def test_public(self): def test_public(self):
@ -212,8 +217,8 @@ class TestLookup(VersionCheckMixin, TestCase):
self.change_status(self.version_1_2_2, amo.STATUS_PENDING) self.change_status(self.version_1_2_2, amo.STATUS_PENDING)
self.addon.reload() self.addon.reload()
assert self.addon.status == amo.STATUS_PUBLIC assert self.addon.status == amo.STATUS_PUBLIC
version, file = self.get('1.2', self.version_int, version, file = self.get_update_instance(
self.app, self.platform) '1.2', self.version_int, self.app, self.platform)
assert version == self.version_1_2_1 assert version == self.version_1_2_1
def test_no_unlisted(self): def test_no_unlisted(self):
@ -224,8 +229,8 @@ class TestLookup(VersionCheckMixin, TestCase):
channel=amo.RELEASE_CHANNEL_UNLISTED) channel=amo.RELEASE_CHANNEL_UNLISTED)
self.addon.reload() self.addon.reload()
assert self.addon.status == amo.STATUS_PUBLIC assert self.addon.status == amo.STATUS_PUBLIC
version, file = self.get('1.2', self.version_int, version, file = self.get_update_instance(
self.app, self.platform) '1.2', self.version_int, self.app, self.platform)
assert version == self.version_1_2_1 assert version == self.version_1_2_1
def test_can_downgrade(self): def test_can_downgrade(self):
@ -236,8 +241,8 @@ class TestLookup(VersionCheckMixin, TestCase):
self.change_status(self.version_1_2_0, amo.STATUS_PENDING) self.change_status(self.version_1_2_0, amo.STATUS_PENDING)
for v in Version.objects.filter(pk__gte=self.version_1_2_1): for v in Version.objects.filter(pk__gte=self.version_1_2_1):
v.delete() v.delete()
version, file = self.get('1.2', self.version_int, version, file = self.get_update_instance(
self.app, self.platform) '1.2', self.version_int, self.app, self.platform)
assert version == self.version_1_1_3 assert version == self.version_1_1_3
@ -252,8 +257,8 @@ class TestLookup(VersionCheckMixin, TestCase):
self.change_status(self.version_1_2_0, amo.STATUS_PENDING) self.change_status(self.version_1_2_0, amo.STATUS_PENDING)
self.change_version(self.version_1_2_0, '1.2beta') self.change_version(self.version_1_2_0, '1.2beta')
version, file = self.get('1.2', self.version_int, version, file = self.get_update_instance(
self.app, self.platform) '1.2', self.version_int, self.app, self.platform)
assert version == self.version_1_2_1 assert version == self.version_1_2_1
@ -267,8 +272,8 @@ class TestLookup(VersionCheckMixin, TestCase):
self.change_version(self.version_1_2_0, '1.2beta') self.change_version(self.version_1_2_0, '1.2beta')
Version.objects.get(pk=self.version_1_2_0).files.all().delete() Version.objects.get(pk=self.version_1_2_0).files.all().delete()
version, file = self.get('1.2beta', self.version_int, version, file = self.get_update_instance(
self.app, self.platform) '1.2beta', self.version_int, self.app, self.platform)
dest = Version.objects.get(pk=self.version_1_2_2) dest = Version.objects.get(pk=self.version_1_2_2)
assert dest.addon.status == amo.STATUS_PUBLIC assert dest.addon.status == amo.STATUS_PUBLIC
assert dest.files.all()[0].status == amo.STATUS_PUBLIC assert dest.files.all()[0].status == amo.STATUS_PUBLIC
@ -281,8 +286,8 @@ class TestLookup(VersionCheckMixin, TestCase):
""" """
self.change_status(self.version_1_2_2, amo.STATUS_NULL) self.change_status(self.version_1_2_2, amo.STATUS_NULL)
self.addon.update(status=amo.STATUS_NULL) self.addon.update(status=amo.STATUS_NULL)
version, file = self.get('1.2.1', self.version_int, version, file = self.get_update_instance(
self.app, self.platform) '1.2.1', self.version_int, self.app, self.platform)
assert version == self.version_1_2_1 assert version == self.version_1_2_1
def test_platform_does_not_exist(self): def test_platform_does_not_exist(self):
@ -292,8 +297,8 @@ class TestLookup(VersionCheckMixin, TestCase):
file.platform = amo.PLATFORM_LINUX.id file.platform = amo.PLATFORM_LINUX.id
file.save() file.save()
version, file = self.get('1.2', self.version_int, version, file = self.get_update_instance(
self.app, self.platform) '1.2', self.version_int, self.app, self.platform)
assert version == self.version_1_2_1 assert version == self.version_1_2_1
def test_platform_exists(self): def test_platform_exists(self):
@ -303,8 +308,8 @@ class TestLookup(VersionCheckMixin, TestCase):
file.platform = amo.PLATFORM_LINUX.id file.platform = amo.PLATFORM_LINUX.id
file.save() file.save()
version, file = self.get('1.2', self.version_int, version, file = self.get_update_instance(
self.app, amo.PLATFORM_LINUX) '1.2', self.version_int, self.app, amo.PLATFORM_LINUX)
assert version == self.version_1_2_2 assert version == self.version_1_2_2
def test_file_for_platform(self): def test_file_for_platform(self):
@ -318,13 +323,13 @@ class TestLookup(VersionCheckMixin, TestCase):
platform=amo.PLATFORM_WIN.id, platform=amo.PLATFORM_WIN.id,
status=amo.STATUS_PUBLIC) status=amo.STATUS_PUBLIC)
file_two.save() file_two.save()
version, file = self.get('1.2', self.version_int, version, file = self.get_update_instance(
self.app, amo.PLATFORM_LINUX) '1.2', self.version_int, self.app, amo.PLATFORM_LINUX)
assert version == self.version_1_2_2 assert version == self.version_1_2_2
assert file == file_one.pk assert file == file_one.pk
version, file = self.get('1.2', self.version_int, version, file = self.get_update_instance(
self.app, amo.PLATFORM_WIN) '1.2', self.version_int, self.app, amo.PLATFORM_WIN)
assert version == self.version_1_2_2 assert version == self.version_1_2_2
assert file == file_two.pk assert file == file_two.pk
@ -385,18 +390,18 @@ class TestDefaultToCompat(VersionCheckMixin, TestCase):
for file in version.files.all(): for file in version.files.all():
file.update(**kw) file.update(**kw)
def get(self, **kw): def get_update_instance(self, **kw):
up = super(TestDefaultToCompat, self).get({ instance = super(TestDefaultToCompat, self).get_update_instance({
'reqVersion': 1, 'reqVersion': 1,
'id': self.addon.guid, 'id': self.addon.guid,
'version': kw.get('item_version', '1.0'), 'version': kw.get('item_version', '1.0'),
'appID': self.app.guid, 'appID': self.app.guid,
'appVersion': kw.get('app_version', '3.0'), 'appVersion': kw.get('app_version', '3.0'),
}) })
assert up.is_valid() assert instance.is_valid()
up.compat_mode = kw.get('compat_mode', 'strict') instance.compat_mode = kw.get('compat_mode', 'strict')
up.get_update() instance.get_update()
return up.data['row'].get('version_id') return instance.data['row'].get('version_id')
def check(self, expected): def check(self, expected):
""" """
@ -408,8 +413,42 @@ class TestDefaultToCompat(VersionCheckMixin, TestCase):
for version in versions: for version in versions:
for mode in modes: for mode in modes:
assert self.get(app_version=version, compat_mode=mode) == ( assert (
expected['-'.join([version, mode])]) self.get_update_instance(
app_version=version, compat_mode=mode) ==
expected['-'.join([version, mode])]
)
def test_application(self):
# Basic test making sure application() is returning the output of
# Update.get_output(). Have to mock Update(): otherwise, the real
# database would be hit, not the test one, because of how services
# use a different setting and database connection APIs.
environ = {
'QUERY_STRING': ''
}
self.start_response_call_count = 0
expected_headers = [
('FakeHeader', 'FakeHeaderValue')
]
expected_output = '{"fake": "output"}'
def start_response_inspector(status, headers):
self.start_response_call_count += 1
assert status == '200 OK'
assert headers == expected_headers
with mock.patch('services.update.Update') as UpdateMock:
update_instance = UpdateMock.return_value
update_instance.get_headers.return_value = expected_headers
update_instance.get_output.return_value = expected_output
output = update.application(environ, start_response_inspector)
assert self.start_response_call_count == 1
# Output is an array with a single string containing the body of the
# response.
assert output == [expected_output]
def test_baseline(self): def test_baseline(self):
# Tests simple add-on (non-binary-components, non-strict). # Tests simple add-on (non-binary-components, non-strict).
@ -523,7 +562,7 @@ class TestResponse(VersionCheckMixin, TestCase):
def setUp(self): def setUp(self):
super(TestResponse, self).setUp() super(TestResponse, self).setUp()
self.addon_one = Addon.objects.get(pk=3615) self.addon_one = Addon.objects.get(pk=3615)
self.good_data = { self.data = {
'id': '{2fa4ed95-0317-4c6a-a74c-5f3e3912c1f9}', 'id': '{2fa4ed95-0317-4c6a-a74c-5f3e3912c1f9}',
'version': '2.0.58', 'version': '2.0.58',
'reqVersion': 1, 'reqVersion': 1,
@ -535,25 +574,35 @@ class TestResponse(VersionCheckMixin, TestCase):
self.win = amo.PLATFORM_WIN self.win = amo.PLATFORM_WIN
def test_bad_guid(self): def test_bad_guid(self):
data = self.good_data.copy() self.data['id'] = 'garbage'
data["id"] = "garbage" instance = self.get_update_instance(self.data)
up = self.get(data) assert instance.use_json is True
assert up.get_rdf() == up.get_bad_rdf() assert json.loads(instance.get_output()) == instance.get_error_output()
# Seamonkey should have a rdf version of 'error ouput'.
self.data['appID'] = '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}'
instance = self.get_update_instance(self.data)
assert instance.use_json is False
result = instance.get_output()
assert result == instance.get_error_output()
rdflib.Graph().parse(data=result)
def test_no_platform(self): def test_no_platform(self):
file = File.objects.get(pk=67442) file = File.objects.get(pk=67442)
file.platform = self.win.id file.platform = self.win.id
file.save() file.save()
data = self.good_data.copy() data = self.data.copy()
data["appOS"] = self.win.api_name data['appOS'] = self.win.api_name
up = self.get(data) instance = self.get_update_instance(data)
assert up.get_rdf() assert instance.get_output()
assert up.data['row']['file_id'] == file.pk assert instance.data['row']['file_id'] == file.pk
data["appOS"] = self.mac.api_name data['appOS'] = self.mac.api_name
up = self.get(data) instance = self.get_update_instance(data)
assert up.get_rdf() == up.get_no_updates_rdf() assert (
json.loads(instance.get_output()) ==
instance.get_no_updates_output())
def test_different_platform(self): def test_different_platform(self):
file = File.objects.get(pk=67442) file = File.objects.get(pk=67442)
@ -566,70 +615,67 @@ class TestResponse(VersionCheckMixin, TestCase):
file.save() file.save()
mac_file_pk = file.pk mac_file_pk = file.pk
data = self.good_data.copy() data = self.data.copy()
data['appOS'] = self.win.api_name data['appOS'] = self.win.api_name
up = self.get(data) instance = self.get_update_instance(data)
up.is_valid() instance.is_valid()
up.get_update() instance.get_update()
assert up.data['row']['file_id'] == file_pk assert instance.data['row']['file_id'] == file_pk
data['appOS'] = self.mac.api_name data['appOS'] = self.mac.api_name
up = self.get(data) instance = self.get_update_instance(data)
up.is_valid() instance.is_valid()
up.get_update() instance.get_update()
assert up.data['row']['file_id'] == mac_file_pk assert instance.data['row']['file_id'] == mac_file_pk
def test_good_version(self): def test_good_version(self):
up = self.get(self.good_data) instance = self.get_update_instance(self.data)
up.is_valid() assert instance.use_json is True
up.get_update() instance.is_valid()
assert up.data['row']['hash'].startswith('sha256:3808b13e') instance.get_update()
assert up.data['row']['min'] == '2.0' assert instance.data['row']['hash'].startswith('sha256:3808b13e')
assert up.data['row']['max'] == '4.0' assert instance.data['row']['min'] == '2.0'
assert instance.data['row']['max'] == '4.0'
def test_no_app_version(self): def test_no_app_version(self):
data = self.good_data.copy() data = self.data.copy()
data['appVersion'] = '1.4' data['appVersion'] = '1.4'
up = self.get(data) instance = self.get_update_instance(data)
up.is_valid() instance.is_valid()
assert not up.get_update() assert not instance.get_update()
def test_low_app_version(self): def test_low_app_version(self):
data = self.good_data.copy() data = self.data.copy()
data['appVersion'] = '2.0' data['appVersion'] = '2.0'
up = self.get(data) instance = self.get_update_instance(data)
up.is_valid() instance.is_valid()
up.get_update() instance.get_update()
assert up.data['row']['hash'].startswith('sha256:3808b13e') assert instance.data['row']['hash'].startswith('sha256:3808b13e')
assert up.data['row']['min'] == '2.0' assert instance.data['row']['min'] == '2.0'
assert up.data['row']['max'] == '4.0' assert instance.data['row']['max'] == '4.0'
def test_content_type(self): def test_content_type(self):
up = self.get(self.good_data) instance = self.get_update_instance(self.data)
('Content-Type', 'text/xml') in up.get_headers(1) ('Content-Type', 'text/xml') in instance.get_headers(1)
def test_cache_control(self): def test_cache_control(self):
up = self.get(self.good_data) instance = self.get_update_instance(self.data)
('Cache-Control', 'public, max-age=3600') in up.get_headers(1) ('Cache-Control', 'public, max-age=3600') in instance.get_headers(1)
def test_length(self): def test_length(self):
up = self.get(self.good_data) instance = self.get_update_instance(self.data)
('Cache-Length', '1') in up.get_headers(1) ('Cache-Length', '1') in instance.get_headers(1)
def test_expires(self): def test_expires(self):
"""Check there are these headers and that expires is 3600 later.""" """Check there are these headers and that expires is 3600 later."""
# We aren't bother going to test the actual time in expires, that # We aren't bother going to test the actual time in expires, that
# way lies pain with broken tests later. # way lies pain with broken tests later.
up = self.get(self.good_data) instance = self.get_update_instance(self.data)
hdrs = dict(up.get_headers(1)) headers = dict(instance.get_headers(1))
lm = datetime(*utils.parsedate_tz(hdrs['Last-Modified'])[:7]) last_modified = datetime(
exp = datetime(*utils.parsedate_tz(hdrs['Expires'])[:7]) *utils.parsedate_tz(headers['Last-Modified'])[:7])
assert (exp - lm).seconds == 3600 expires = datetime(*utils.parsedate_tz(headers['Expires'])[:7])
assert (expires - last_modified).seconds == 3600
def test_appguid(self):
up = self.get(self.good_data)
rdf = up.get_rdf()
assert rdf.find(self.good_data['appID']) > -1
def get_file_url(self): def get_file_url(self):
"""Return the file url with the hash as parameter.""" """Return the file url with the hash as parameter."""
@ -638,39 +684,47 @@ class TestResponse(VersionCheckMixin, TestCase):
'e09fef55f2673458bc31f') 'e09fef55f2673458bc31f')
def test_url(self): def test_url(self):
up = self.get(self.good_data) instance = self.get_update_instance(self.data)
up.get_rdf() instance.get_output()
assert up.data['row']['url'] == self.get_file_url() assert instance.data['row']['url'] == self.get_file_url()
def test_url_local_recent(self): def test_url_local_recent(self):
a_bit_ago = datetime.now() - timedelta(seconds=60) a_bit_ago = datetime.now() - timedelta(seconds=60)
File.objects.get(pk=67442).update(datestatuschanged=a_bit_ago) File.objects.get(pk=67442).update(datestatuschanged=a_bit_ago)
up = self.get(self.good_data) instance = self.get_update_instance(self.data)
up.get_rdf() instance.get_output()
assert up.data['row']['url'] == self.get_file_url() assert instance.data['row']['url'] == self.get_file_url()
def test_hash(self): def test_hash(self):
rdf = self.get(self.good_data).get_rdf() content = self.get_update_instance(self.data).get_output()
assert rdf.find('updateHash') > -1 data = json.loads(content)
file = File.objects.get(pk=67442)
guid = '{2fa4ed95-0317-4c6a-a74c-5f3e3912c1f9}'
assert data['addons'][guid]['updates'][0]['update_hash'] == file.hash
file = File.objects.get(pk=67442) file = File.objects.get(pk=67442)
file.hash = '' file.hash = ''
file.save() file.save()
rdf = self.get(self.good_data).get_rdf() content = self.get_update_instance(self.data).get_output()
assert rdf.find('updateHash') == -1 data = json.loads(content)
assert 'update_hash' not in data['addons'][guid]['updates'][0]
def test_releasenotes(self): def test_releasenotes(self):
rdf = self.get(self.good_data).get_rdf() content = self.get_update_instance(self.data).get_output()
assert rdf.find('updateInfoURL') > -1 data = json.loads(content)
guid = '{2fa4ed95-0317-4c6a-a74c-5f3e3912c1f9}'
assert data['addons'][guid]['updates'][0]['update_info_url']
version = Version.objects.get(pk=81551) version = Version.objects.get(pk=81551)
version.update(releasenotes=None) version.update(releasenotes=None)
rdf = self.get(self.good_data).get_rdf() content = self.get_update_instance(self.data).get_output()
assert rdf.find('updateInfoURL') == -1 data = json.loads(content)
assert 'update_info_url' not in data['addons'][guid]['updates'][0]
def test_sea_monkey(self): def test_seamonkey_serve_rdf(self):
data = { data = {
'id': 'bettergmail2@ginatrapani.org', 'id': 'bettergmail2@ginatrapani.org',
'version': '1', 'version': '1',
@ -678,107 +732,35 @@ class TestResponse(VersionCheckMixin, TestCase):
'reqVersion': 1, 'reqVersion': 1,
'appVersion': '1.0', 'appVersion': '1.0',
} }
up = self.get(data) instance = self.get_update_instance(data)
rdf = up.get_rdf() result = instance.get_output()
assert up.data['row']['hash'].startswith('sha256:9d9a389') assert instance.data['row']['hash'].startswith('sha256:9d9a389')
assert up.data['row']['min'] == '1.0' assert instance.data['row']['min'] == '1.0'
assert up.data['row']['version'] == '0.5.2' assert instance.data['row']['version'] == '0.5.2'
assert rdf.find(data['appID']) > -1
# Result should be a valid rdf.
rdflib.Graph().parse(data=result)
def test_no_updates_at_all(self): def test_no_updates_at_all(self):
self.addon_one.versions.all().delete() self.addon_one.versions.all().delete()
upd = self.get(self.good_data) instance = self.get_update_instance(self.data)
assert upd.get_rdf() == upd.get_no_updates_rdf() assert instance.use_json is True
assert (
json.loads(instance.get_output()) ==
instance.get_no_updates_output())
# Seamonkey should have a rdf version of 'no updates'.
self.data['appID'] = '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}'
instance = self.get_update_instance(self.data)
assert instance.use_json is False
result = instance.get_output()
assert result == instance.get_no_updates_output()
rdflib.Graph().parse(data=result)
def test_no_updates_my_fx(self): def test_no_updates_my_fx(self):
data = self.good_data.copy() data = self.data.copy()
data['appVersion'] = '5.0.1' data['appVersion'] = '5.0.1'
upd = self.get(data) instance = self.get_update_instance(data)
assert upd.get_rdf() == upd.get_no_updates_rdf() assert (
json.loads(instance.get_output()) ==
instance.get_no_updates_output())
class TestFirefoxHotfix(VersionCheckMixin, TestCase):
def setUp(self):
"""Create a "firefox hotfix" addon with a few versions.
Check bug 1031516 for more info.
"""
super(TestFirefoxHotfix, self).setUp()
self.addon = amo.tests.addon_factory(guid='firefox-hotfix@mozilla.org')
# First signature changing hotfix.
amo.tests.version_factory(addon=self.addon, version='20121019.01',
min_app_version='10.0',
max_app_version='16.*')
# Second signature changing hotfix.
amo.tests.version_factory(addon=self.addon, version='20130826.01',
min_app_version='10.0',
max_app_version='24.*')
# Newest version compatible with any Firefox.
amo.tests.version_factory(addon=self.addon, version='20202020.01',
min_app_version='10.0',
max_app_version='30.*')
self.data = {
'id': 'firefox-hotfix@mozilla.org',
'appID': '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}',
'reqVersion': '2',
}
def test_10_16_first_hotfix(self):
"""The first hotfix changing the signature should be served."""
self.data['version'] = ''
self.data['appVersion'] = '16.0.1'
up = self.get(self.data)
rdf = up.get_rdf()
assert rdf.find('20121019.01') > -1
def test_10_16_second_hotfix(self):
"""The second hotfix changing the signature should be served."""
self.data['version'] = '20121019.01'
self.data['appVersion'] = '16.0.1'
up = self.get(self.data)
rdf = up.get_rdf()
assert rdf.find('20130826.01') > -1
def test_10_16_newest_hotfix(self):
"""The newest hotfix should be served."""
self.data['version'] = '20130826.01'
self.data['appVersion'] = '16.0.1'
up = self.get(self.data)
rdf = up.get_rdf()
assert rdf.find('20202020.01') > -1
def test_16_24_second_hotfix(self):
"""The second hotfix changing the signature should be served."""
self.data['version'] = ''
self.data['appVersion'] = '16.0.2'
up = self.get(self.data)
rdf = up.get_rdf()
assert rdf.find('20130826.01') > -1
def test_16_24_newest_hotfix(self):
"""The newest hotfix should be served."""
self.data['version'] = '20130826.01'
self.data['appVersion'] = '16.0.2'
up = self.get(self.data)
rdf = up.get_rdf()
assert rdf.find('20202020.01') > -1
def test_above_24_latest_version(self):
"""The newest hotfix should be served."""
self.data['version'] = ''
self.data['appVersion'] = '28.0'
up = self.get(self.data)
rdf = up.get_rdf()
assert rdf.find('20202020.01') > -1