From d0c435bca047cd71a956c7efbd813ff007d934b4 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Tue, 19 Feb 2019 08:01:14 +0100 Subject: [PATCH] Remove Legacy API (#10705) Fixes #9274 * Part 1: Remove legacy api * Smaller cleanups * Restore opensearch * Remove CompatOverride.is_hosted * Remove one more test * Fix AuthenticationMiddlewareWithoutAPI middleware --- src/olympia/addons/models.py | 4 - src/olympia/addons/tests/test_models.py | 8 - src/olympia/amo/middleware.py | 8 +- src/olympia/amo/tests/test_middleware.py | 8 - src/olympia/amo/tests/test_redirects.py | 7 - src/olympia/amo/tests/test_url_prefix.py | 12 - src/olympia/amo/tests/test_views.py | 6 - src/olympia/amo/urls.py | 8 +- src/olympia/amo/utils.py | 32 + src/olympia/amo/views.py | 17 +- src/olympia/bandwagon/urls.py | 2 - src/olympia/bandwagon/views.py | 21 +- src/olympia/legacy_api/__init__.py | 3 - src/olympia/legacy_api/models.py | 0 .../templates/legacy_api/addon_detail.xml | 2 - .../templates/legacy_api/compat.xml | 27 - .../templates/legacy_api/includes/addon.xml | 130 -- .../legacy_api/templates/legacy_api/list.xml | 6 - .../templates/legacy_api/message.xml | 11 - .../templates/legacy_api/search.xml | 13 - src/olympia/legacy_api/tests/__init__.py | 0 src/olympia/legacy_api/tests/test_utils.py | 13 - src/olympia/legacy_api/tests/test_views.py | 1253 ----------------- src/olympia/legacy_api/urls.py | 74 - src/olympia/legacy_api/utils.py | 221 --- src/olympia/legacy_api/views.py | 493 ------- src/olympia/legacy_discovery/modules.py | 88 +- src/olympia/lib/settings_base.py | 1 - .../search/tests/test_search_ranking.py | 10 +- src/olympia/urls.py | 3 - 30 files changed, 126 insertions(+), 2355 deletions(-) delete mode 100644 src/olympia/legacy_api/__init__.py delete mode 100644 src/olympia/legacy_api/models.py delete mode 100644 src/olympia/legacy_api/templates/legacy_api/addon_detail.xml delete mode 100644 src/olympia/legacy_api/templates/legacy_api/compat.xml delete mode 100644 src/olympia/legacy_api/templates/legacy_api/includes/addon.xml delete mode 100644 src/olympia/legacy_api/templates/legacy_api/list.xml delete mode 100644 src/olympia/legacy_api/templates/legacy_api/message.xml delete mode 100644 src/olympia/legacy_api/templates/legacy_api/search.xml delete mode 100644 src/olympia/legacy_api/tests/__init__.py delete mode 100644 src/olympia/legacy_api/tests/test_utils.py delete mode 100644 src/olympia/legacy_api/tests/test_views.py delete mode 100644 src/olympia/legacy_api/urls.py delete mode 100644 src/olympia/legacy_api/utils.py delete mode 100644 src/olympia/legacy_api/views.py diff --git a/src/olympia/addons/models.py b/src/olympia/addons/models.py index 396195fc00..b30b6c2ec8 100644 --- a/src/olympia/addons/models.py +++ b/src/olympia/addons/models.py @@ -2054,10 +2054,6 @@ class CompatOverride(ModelBase): else: return self.guid - def is_hosted(self): - """Am I talking about an add-on on AMO?""" - return bool(self.addon_id) - @staticmethod def transformer(overrides): if not overrides: diff --git a/src/olympia/addons/tests/test_models.py b/src/olympia/addons/tests/test_models.py index afb8247326..b96ee4a482 100644 --- a/src/olympia/addons/tests/test_models.py +++ b/src/olympia/addons/tests/test_models.py @@ -2765,14 +2765,6 @@ class TestCompatOverride(TestCase): actual = getattr(obj, key) assert actual == expected - def test_is_hosted(self): - c = CompatOverride.objects.create(guid='a') - assert not c.is_hosted() - - Addon.objects.create(type=1, guid='b') - c = CompatOverride.objects.create(guid='b') - assert c.is_hosted() - def test_override_type(self): one = CompatOverride.objects.get(guid='one') diff --git a/src/olympia/amo/middleware.py b/src/olympia/amo/middleware.py index 14029cd44a..cd4b20aec2 100644 --- a/src/olympia/amo/middleware.py +++ b/src/olympia/amo/middleware.py @@ -97,11 +97,6 @@ class LocaleAndAppURLMiddleware(MiddlewareMixin): activate(request.LANG) request.APP = amo.APPS.get(prefixer.app, amo.FIREFOX) - # Match legacy api requests too - IdentifyAPIRequestMiddleware is v3+ - # TODO - remove this when legacy_api goes away - # https://github.com/mozilla/addons-server/issues/9274 - request.is_legacy_api = request.path_info.startswith('/api/') - class AuthenticationMiddlewareWithoutAPI(AuthenticationMiddleware): """ @@ -109,8 +104,7 @@ class AuthenticationMiddlewareWithoutAPI(AuthenticationMiddleware): own authentication mechanism. """ def process_request(self, request): - legacy_or_drf_api = request.is_api or request.is_legacy_api - if legacy_or_drf_api and not auth_path.match(request.path): + if request.is_api and not auth_path.match(request.path): request.user = AnonymousUser() else: return super( diff --git a/src/olympia/amo/tests/test_middleware.py b/src/olympia/amo/tests/test_middleware.py index 14118a769c..a09372d735 100644 --- a/src/olympia/amo/tests/test_middleware.py +++ b/src/olympia/amo/tests/test_middleware.py @@ -37,7 +37,6 @@ class TestMiddleware(TestCase): def test_authentication_used_outside_the_api(self, process_request): req = RequestFactory().get('/') req.is_api = False - req.is_legacy_api = False AuthenticationMiddlewareWithoutAPI().process_request(req) assert process_request.called @@ -46,13 +45,6 @@ class TestMiddleware(TestCase): def test_authentication_not_used_with_the_api(self, process_request): req = RequestFactory().get('/') req.is_api = True - req.is_legacy_api = False - AuthenticationMiddlewareWithoutAPI().process_request(req) - assert not process_request.called - - req = RequestFactory().get('/') - req.is_api = False - req.is_legacy_api = True AuthenticationMiddlewareWithoutAPI().process_request(req) assert not process_request.called diff --git a/src/olympia/amo/tests/test_redirects.py b/src/olympia/amo/tests/test_redirects.py index 13549b8ae0..53ff7ab6a4 100644 --- a/src/olympia/amo/tests/test_redirects.py +++ b/src/olympia/amo/tests/test_redirects.py @@ -30,13 +30,6 @@ class TestRedirects(TestCase): redirect = response.redirect_chain[-1][0] assert redirect.endswith('/en-US/firefox/addon/5326/') - def test_utf8(self): - """Without proper unicode handling this will fail.""" - response = self.client.get(u'/api/1.5/search/ツールバー', - follow=True) - # Sphinx will be off so let's just test that it redirects. - assert response.redirect_chain[0][1] == 302 - def test_reviews(self): response = self.client.get('/reviews/display/4', follow=True) self.assert3xx(response, '/en-US/firefox/addon/a4/reviews/', diff --git a/src/olympia/amo/tests/test_url_prefix.py b/src/olympia/amo/tests/test_url_prefix.py index 85a58999ab..d680764708 100644 --- a/src/olympia/amo/tests/test_url_prefix.py +++ b/src/olympia/amo/tests/test_url_prefix.py @@ -81,18 +81,6 @@ class MiddlewareTest(TestCase): response = self.process('/api/v4/some/endpoint/') assert response is None - def test_v1_api_is_identified_as_api_request(self): - response = self.process('/en-US/firefox/api/') - assert response is None - assert self.request.LANG == 'en-US' - assert self.request.is_legacy_api - - # double-check _only_ /api/ is marked as .is_api - response = self.process('/en-US/firefox/apii/') - assert response is None - assert self.request.LANG == 'en-US' - assert not self.request.is_legacy_api - def test_vary(self): response = self.process('/') assert response['Vary'] == 'Accept-Language, User-Agent' diff --git a/src/olympia/amo/tests/test_views.py b/src/olympia/amo/tests/test_views.py index 9dd95fa0f9..6c308a86e2 100644 --- a/src/olympia/amo/tests/test_views.py +++ b/src/olympia/amo/tests/test_views.py @@ -384,12 +384,6 @@ class TestCORS(TestCase): assert not response.has_header('Access-Control-Allow-Origin') assert not response.has_header('Access-Control-Allow-Credentials') - def test_no_cors_legacy_api(self): - response = self.get('/en-US/firefox/api/1.5/search/test') - assert response.status_code == 200 - assert not response.has_header('Access-Control-Allow-Origin') - assert not response.has_header('Access-Control-Allow-Credentials') - def test_cors_api_v3(self): url = reverse_ns('addon-detail', api_version='v3', args=(3615,)) assert '/api/v3/' in url diff --git a/src/olympia/amo/urls.py b/src/olympia/amo/urls.py index 1a96377041..2c0d55cb75 100644 --- a/src/olympia/amo/urls.py +++ b/src/olympia/amo/urls.py @@ -1,9 +1,8 @@ from django.conf.urls import include, url from django.views.decorators.cache import never_cache -from olympia.legacy_api import views as legacy_views - from . import views +from .utils import render_xml services_patterns = [ @@ -20,8 +19,7 @@ urlpatterns = [ url(r'^contribute\.json$', views.contribute, name='contribute.json'), url(r'^services/', include(services_patterns)), url(r'^__version__$', views.version, name='version.json'), - - url(r'^opensearch\.xml$', legacy_views.render_xml, - {'template': 'amo/opensearch.xml'}, + url(r'^opensearch\.xml$', render_xml, {'template': 'amo/opensearch.xml'}, name='amo.opensearch'), + ] diff --git a/src/olympia/amo/utils.py b/src/olympia/amo/utils.py index 2cc3ec06bf..93369bfc76 100644 --- a/src/olympia/amo/utils.py +++ b/src/olympia/amo/utils.py @@ -49,6 +49,8 @@ from html5lib.serializer import HTMLSerializer from PIL import Image from rest_framework.utils.encoders import JSONEncoder +from django.db.transaction import non_atomic_requests + from olympia.core.logger import getLogger from olympia.amo import ADDON_ICON_SIZES, search from olympia.amo.pagination import ESPaginator @@ -71,6 +73,36 @@ def from_string(string): return engines['jinja2'].from_string(string) +def render_xml_to_string(request, template, context=None): + from olympia.amo.templatetags.jinja_helpers import strip_controls + + if context is None: + context = {} + + xml_env = engines['jinja2'].env.overlay() + old_finalize = xml_env.finalize + xml_env.finalize = lambda x: strip_controls(old_finalize(x)) + + for processor in engines['jinja2'].context_processors: + context.update(processor(request)) + + template = xml_env.get_template(template) + return template.render(context) + + +@non_atomic_requests +def render_xml(request, template, context=None, **kwargs): + """Safely renders xml, stripping out nasty control characters.""" + if context is None: + context = {} + rendered = render_xml_to_string(request, template, context) + + if 'content_type' not in kwargs: + kwargs['content_type'] = 'text/xml' + + return HttpResponse(rendered, **kwargs) + + def days_ago(n): return datetime.datetime.now() - datetime.timedelta(days=n) diff --git a/src/olympia/amo/views.py b/src/olympia/amo/views.py index cd17d53097..ca5130f027 100644 --- a/src/olympia/amo/views.py +++ b/src/olympia/amo/views.py @@ -13,7 +13,7 @@ import six from django_statsd.clients import statsd from rest_framework.exceptions import NotFound -from olympia import amo, legacy_api +from olympia import amo from olympia.amo.utils import render from . import monitors @@ -68,11 +68,7 @@ def contribute(request): @non_atomic_requests def handler403(request, **kwargs): - if request.is_legacy_api: - # Pass over to handler403 view in api if api was targeted. - return legacy_api.views.handler403(request) - else: - return render(request, 'amo/403.html', status=403) + return render(request, 'amo/403.html', status=403) @non_atomic_requests @@ -81,9 +77,6 @@ def handler404(request, **kwargs): # It's a v3+ api request return JsonResponse( {'detail': six.text_type(NotFound.default_detail)}, status=404) - elif request.is_legacy_api: - # It's a legacy api request - pass over to legacy api handler404. - return legacy_api.views.handler404(request) # X_IS_MOBILE_AGENTS is set by nginx as an env variable when it detects # a mobile User Agent or when the mamo cookie is present. if request.META.get('X_IS_MOBILE_AGENTS') == '1': @@ -94,11 +87,7 @@ def handler404(request, **kwargs): @non_atomic_requests def handler500(request, **kwargs): - if getattr(request, 'is_legacy_api', False): - # Pass over to handler500 view in api if api was targeted. - return legacy_api.views.handler500(request) - else: - return render(request, 'amo/500.html', status=500) + return render(request, 'amo/500.html', status=500) @non_atomic_requests diff --git a/src/olympia/bandwagon/urls.py b/src/olympia/bandwagon/urls.py index 4bcffba7b2..e66b9227fb 100644 --- a/src/olympia/bandwagon/urls.py +++ b/src/olympia/bandwagon/urls.py @@ -11,8 +11,6 @@ edit_urls = [ detail_urls = [ url(r'^$', views.collection_detail, name='collections.detail'), - url(r'^format:json$', views.collection_detail_json, - name='collections.detail.json'), url(r'^edit/', include(edit_urls)), url(r'^delete$', views.delete, name='collections.delete'), url(r'^(?Padd|remove)$', views.collection_alter, diff --git a/src/olympia/bandwagon/views.py b/src/olympia/bandwagon/views.py index 828b194844..8b4fd93e92 100644 --- a/src/olympia/bandwagon/views.py +++ b/src/olympia/bandwagon/views.py @@ -24,13 +24,12 @@ from olympia.addons.models import Addon from olympia.addons.views import BaseFilter from olympia.amo import messages from olympia.amo.decorators import ( - allow_mine, json_view, login_required, post_required, use_primary_db) + allow_mine, login_required, post_required, use_primary_db) from olympia.amo.urlresolvers import reverse from olympia.amo.utils import paginate, render, urlparams from olympia.api.filters import OrderingAliasFilter from olympia.api.permissions import ( AllOf, AllowReadOnlyIfPublic, AnyOf, PreventActionPermission) -from olympia.legacy_api.utils import addon_to_dict from olympia.translations.query import order_by_translation from olympia.users.decorators import process_user_id from olympia.users.models import UserProfile @@ -194,24 +193,6 @@ def collection_detail(request, user_id, slug): 'user_perms': user_perms}) -@json_view(has_trans=True) -@allow_mine -@process_user_id -@non_atomic_requests -def collection_detail_json(request, user_id, slug): - collection = get_collection(request, user_id, slug) - if not (collection.listed or acl.check_collection_ownership( - request, collection)): - raise PermissionDenied - # We evaluate the QuerySet with `list` to work around bug 866454. - addons_dict = [addon_to_dict(a) for a in list(collection.addons.valid())] - return { - 'name': collection.name, - 'url': collection.get_abs_url(), - 'addons': addons_dict - } - - def get_notes(collection, raw=False): # This might hurt in a big collection with lots of notes. # It's a generator so we don't evaluate anything by default. diff --git a/src/olympia/legacy_api/__init__.py b/src/olympia/legacy_api/__init__.py deleted file mode 100644 index c77f97f73d..0000000000 --- a/src/olympia/legacy_api/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -MIN_VERSION = 1.0 -CURRENT_VERSION = 1.5 -MAX_VERSION = CURRENT_VERSION diff --git a/src/olympia/legacy_api/models.py b/src/olympia/legacy_api/models.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/olympia/legacy_api/templates/legacy_api/addon_detail.xml b/src/olympia/legacy_api/templates/legacy_api/addon_detail.xml deleted file mode 100644 index aba3012b04..0000000000 --- a/src/olympia/legacy_api/templates/legacy_api/addon_detail.xml +++ /dev/null @@ -1,2 +0,0 @@ - -{% include 'legacy_api/includes/addon.xml' %} diff --git a/src/olympia/legacy_api/templates/legacy_api/compat.xml b/src/olympia/legacy_api/templates/legacy_api/compat.xml deleted file mode 100644 index d72317b094..0000000000 --- a/src/olympia/legacy_api/templates/legacy_api/compat.xml +++ /dev/null @@ -1,27 +0,0 @@ -{% for c in compat %} - - {{ c.guid }} - {{ c.name }} - - {% for vr in c.collapsed_ranges() %} - - {{ vr.min }} - {{ vr.max }} - - {% for app in vr.apps %} - - {{ app.app.pretty }} - {{ app.app.id }} - {{ app.min }} - {{ app.max }} - {{ app.app.guid }} - - {% endfor %} - - - {% endfor %} - - -{% endfor %} diff --git a/src/olympia/legacy_api/templates/legacy_api/includes/addon.xml b/src/olympia/legacy_api/templates/legacy_api/includes/addon.xml deleted file mode 100644 index 34c460710c..0000000000 --- a/src/olympia/legacy_api/templates/legacy_api/includes/addon.xml +++ /dev/null @@ -1,130 +0,0 @@ -{% set new_api = (api_version >= 1.5) %} -{% if addon.compat_version %} - {% set version = addon.compat_version %} -{% else %} - {% set version = addon.current_version %} -{% endif %} - - {{ addon.name }} - {{ amo.ADDON_TYPE[addon.type] }} - {{ addon.guid }} - {{ addon.slug }} - {{ version.version }} - {{ addon.STATUS_CHOICES[addon.status] }} - - {% if not addon.is_persona() %} - {% for author in addon.listed_authors -%} - {%- if new_api -%} - - {{ author.name }} - {{ author.get_url_path()|absolutify|urlparams(src='api') }} - - {%- else -%} - {{ author.name }} - {%- endif -%} - {%- endfor %} - {% endif %} - - {%- if new_api -%}{{ addon.summary }}{% else %}{{ addon.summary|clean(true) }}{% endif -%} - {%- if new_api %}{{ addon.description }}{% else -%}{{ addon.description|clean(true) }}{% endif %} - {%- if version and version.license -%} - - {{ version.license.name }} - - {%- if not version.license.url -%} - {{ version.license_url()|absolutify }} - {%- else -%} - {{ version.license.url|absolutify }} - {%- endif -%} - - - {%- endif -%} - {# The 32px icon must be the last icon specified. #} - {{ addon.get_icon_url(64, use_default=False) }} - {{ addon.get_icon_url(32, use_default=False) }} - - {%- if version -%} - {%- for app in version.compatible_apps.values() %} - {%- if amo.APP_IDS.get(app.application) -%} - - {{ amo.APP_IDS[app.application].pretty }} - {{ app.application }} - {{ app.min }} - {{ app.max }} - {{ amo.APP_IDS[app.application].guid }} - - {%- endif -%} - {%- endfor -%} - {%- endif -%} - - - {%- if version -%} - {% for os in version.supported_platforms -%} - {{ os.api_name }} - {%- endfor -%} - {%- endif -%} - - {%- if new_api %}{{ addon.eula }}{% else %}{{ addon.eula|clean(true) }}{% endif -%} - {%- if new_api -%} - - {%- for preview in addon.current_previews -%} - - - {{ preview.image_url|urlparams(src='api') }} - - - {{ preview.thumbnail_url|urlparams(src='api') }} - - {%- if preview.caption -%} - {{ preview.caption }} - {%- endif -%} - - {%- endfor -%} - - {%- else -%} - {{ addon.thumbnail_url }} - {%- endif -%} - {{ addon.average_rating|round|int }} - {{ addon.get_url_path()|absolutify|urlparams(src='api') }} - {%- if version -%} - {%- for file in version.all_files -%} - {{ file.get_url_path('api') }} - {% endfor -%} - {%- endif -%} - {%- if new_api %} - {%- if addon.contributions -%} - - - {{ addon.contributions }} - - - {%- endif -%} - {{ addon.developer_comments }} - - {{ addon.ratings_url|absolutify|urlparams(src='api') }} - - {{ addon.total_downloads }} - {{ addon.weekly_downloads }} - {{ addon.average_daily_users }} - - {{ addon.created|isotime }} - - - {{ addon.last_updated|isotime }} - - {{ addon.homepage }} - {{ addon.support_url }} - {{ addon.is_featured(request.APP, request.LANG)|int }} - {# Only kept for backwards compatibility of the API #} - - - - {% if show_localepicker -%} - - {% endif %} - {%- endif -%} - diff --git a/src/olympia/legacy_api/templates/legacy_api/list.xml b/src/olympia/legacy_api/templates/legacy_api/list.xml deleted file mode 100644 index 0569785cd9..0000000000 --- a/src/olympia/legacy_api/templates/legacy_api/list.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - {% for addon in addons %} - {% include 'legacy_api/includes/addon.xml' %} - {% endfor %} - diff --git a/src/olympia/legacy_api/templates/legacy_api/message.xml b/src/olympia/legacy_api/templates/legacy_api/message.xml deleted file mode 100644 index 7f42224c3e..0000000000 --- a/src/olympia/legacy_api/templates/legacy_api/message.xml +++ /dev/null @@ -1,11 +0,0 @@ - -{# Compatibility across both Piston and DRF on error messages/vars. #} -{% if detail %} - {{_(detail)}} -{% else %} - {% if error_level %} - {{_(msg)}} - {% else %} - {{_(msg)}} - {% endif %} -{% endif %} diff --git a/src/olympia/legacy_api/templates/legacy_api/search.xml b/src/olympia/legacy_api/templates/legacy_api/search.xml deleted file mode 100644 index 686c43a205..0000000000 --- a/src/olympia/legacy_api/templates/legacy_api/search.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - {% if addons_xml %} - {% for xml in addons_xml %} - {{ xml|xssafe }} - {% endfor %} - {% else %} - {% for addon in results -%} - {% include 'legacy_api/includes/addon.xml' -%} - {% endfor %} - {% endif %} - {% if compat %}{% include 'legacy_api/compat.xml' %}{% endif %} - diff --git a/src/olympia/legacy_api/tests/__init__.py b/src/olympia/legacy_api/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/olympia/legacy_api/tests/test_utils.py b/src/olympia/legacy_api/tests/test_utils.py deleted file mode 100644 index 91da050ebc..0000000000 --- a/src/olympia/legacy_api/tests/test_utils.py +++ /dev/null @@ -1,13 +0,0 @@ -from olympia import amo -from olympia.amo.tests import TestCase, addon_factory, version_factory -from olympia.legacy_api.utils import find_compatible_version - - -class TestCompatibleVersion(TestCase): - def test_compatible_version(self): - addon = addon_factory() - version = version_factory(addon=addon, version='99') - version_factory( - addon=addon, version='100', - channel=amo.RELEASE_CHANNEL_UNLISTED) - assert find_compatible_version(addon, amo.FIREFOX.id) == version diff --git a/src/olympia/legacy_api/tests/test_views.py b/src/olympia/legacy_api/tests/test_views.py deleted file mode 100644 index a711d24ea6..0000000000 --- a/src/olympia/legacy_api/tests/test_views.py +++ /dev/null @@ -1,1253 +0,0 @@ -# -*- coding: utf-8 -*- -import json - -from textwrap import dedent - -from django.conf import settings -from django.test.client import Client -from django.utils import translation -from django.utils.encoding import force_text - -import jinja2 -import pytest -import six - -from mock import patch -from pyquery import PyQuery as pq - -from olympia import amo, legacy_api -from olympia.addons.models import ( - Addon, AppSupport, CompatOverride, CompatOverrideRange, Persona, Preview) -from olympia.amo.templatetags import jinja_helpers -from olympia.amo.tests import ESTestCase, TestCase, addon_factory -from olympia.amo.urlresolvers import reverse -from olympia.amo.views import handler500 -from olympia.applications.models import AppVersion -from olympia.bandwagon.models import ( - Collection, CollectionAddon, FeaturedCollection) -from olympia.files.models import File -from olympia.files.tests.test_models import UploadTest -from olympia.legacy_api.utils import addon_to_dict -from olympia.legacy_api.views import addon_filter -from olympia.tags.models import AddonTag, Tag - - -pytestmark = pytest.mark.django_db - - -def api_url(x, app='firefox', lang='en-US', version=1.5): - return '/%s/%s/api/%s/%s' % (lang, app, version, x) - - -client = Client() - - -def make_call(*args, **kwargs): - return client.get(api_url(*args, **kwargs)) - - -def test_json_not_implemented(): - assert legacy_api.views.APIView().render_json({}) == ( - '{"msg": "Not implemented yet."}') - - -class UtilsTest(TestCase): - fixtures = ['base/addon_3615'] - - def setUp(self): - super(UtilsTest, self).setUp() - self.a = Addon.objects.get(pk=3615) - - def test_dict(self): - """Verify that we're getting dict.""" - d = addon_to_dict(self.a) - assert d, 'Add-on dictionary not found' - assert d['learnmore'].endswith('/addon/a3615/?src=api'), ( - 'Add-on details URL does not end with "?src=api"') - - def test_sanitize(self): - """Check that tags are stripped for summary and description.""" - self.a.summary = self.a.description = 'i <3 amo!' - self.a.save() - d = addon_to_dict(self.a) - assert d['summary'] == 'i <3 amo!' - assert d['description'] == 'i <3 amo!' - - def test_simple_contributions(self): - self.a.update(contributions='https://paypal.me/blah') - d = addon_to_dict(self.a) - assert d['contribution']['meet_developers'] == self.a.contributions - assert 'link' not in d['contribution'] - - -class No500ErrorsTest(TestCase): - """ - A series of unfortunate urls that have caused 500 errors in the past. - """ - def test_search_bad_type(self): - """ - For search/:term/:addon_type <-- addon_type should be an integer. - """ - response = make_call('/search/foo/theme') - # We'll likely get a 503 since Sphinx is off and that - # is good. We just don't want 500 errors. - assert response.status_code != 500, "We received a 500 error, wtf?" - - def test_list_bad_type(self): - """ - For list/new/:addon_type <-- addon_type should be an integer. - """ - response = make_call('/list/new/extension') - assert response.status_code != 500, "We received a 500 error, wtf?" - - def test_utf_redirect(self): - """Test that urls with unicode redirect properly.""" - response = make_call(u'search/ツールバー', version=1.5) - assert response.status_code != 500, "Unicode failed to redirect." - - def test_manual_utf_search(self): - """If someone searches for non doubly encoded data using an old API we - should not try to decode it.""" - response = make_call(u'search/für', version=1.2) - assert response.status_code != 500, "ZOMG Unicode fails." - - def test_broken_guid(self): - response = make_call(u'search/guid:+972"e4c6-}', version=1.5) - assert response.status_code != 500, "Failed to cope with guid" - - -class ControlCharacterTest(TestCase): - """This test is to assure we aren't showing control characters.""" - - fixtures = ('base/addon_3615',) - - def test(self): - addon = Addon.objects.get(pk=3615) - char = chr(12) - addon.name = "I %sove You" % char - addon.save() - response = make_call('addon/3615') - self.assertNotContains(response, char) - - -class StripHTMLTest(TestCase): - fixtures = ('base/addon_3615',) - - def test(self): - """For API < 1.5 we remove HTML.""" - addon = Addon.objects.get(pk=3615) - addon.eula = 'free stock tips' - addon.summary = 'xxx videos' - addon.description = 'FFFFUUUU' - addon.save() - - response = make_call('addon/3615', version=1.5) - doc = pq(response.content) - assert doc('eula').html() == 'free stock tips' - assert doc('summary').html() == '<i>xxx video</i>s' - assert doc('description').html() == 'FFFFUUUU' - - response = make_call('addon/3615', version=1.2) - doc = pq(response.content) - assert doc('eula').html() == 'free stock tips' - assert doc('summary').html() == 'xxx videos' - assert doc('description').html() == 'FFFFUUUU' - - -class APITest(TestCase): - fixtures = ['base/addon_3615', 'base/addon_4664_twitterbar', - 'base/addon_5299_gcal'] - - def test_api_caching(self): - response = self.client.get('/en-US/firefox/api/1.5/addon/3615') - assert response.status_code == 200 - self.assertContains(response, '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}') - - def test_addon_detail_empty_eula(self): - """ - Empty EULA should show up as '' not None. See - https://bugzilla.mozilla.org/show_bug.cgi?id=546542. - """ - response = self.client.get('/en-US/firefox/api/%.1f/addon/4664' % - legacy_api.CURRENT_VERSION) - self.assertContains(response, '') - - def test_addon_detail_rating(self): - a = Addon.objects.get(pk=4664) - response = self.client.get('/en-US/firefox/api/%.1f/addon/4664' % - legacy_api.CURRENT_VERSION) - self.assertContains(response, '%d' % - int(round(a.average_rating))) - - def test_addon_detail_xml(self): - response = self.client.get('/en-US/firefox/api/%.1f/addon/3615' % 1.2) - - self.assertContains(response, "Delicious Bookmarks") - self.assertContains(response, """id="1">Extension""") - self.assertContains( - response, "{2fa4ed95-0317-4c6a-a74c-5f3e3912c1f9}") - self.assertContains(response, "2.1.072") - self.assertContains(response, 'Approved') - self.assertContains( - response, u'55021 \u0627\u0644\u062a\u0637\u0628') - self.assertContains(response, "Delicious Bookmarks is the") - self.assertContains(response, "This extension integrates") - - icon_url = "%s3/3615-32.png" % jinja_helpers.user_media_url( - 'addon_icons') - self.assertContains(response, '' + icon_url) - self.assertContains(response, "") - self.assertContains(response, "Firefox") - self.assertContains(response, "1") - self.assertContains(response, "2.0") - self.assertContains(response, "4.0") - self.assertContains(response, "ALL") - self.assertContains(response, "") - self.assertContains(response, "/icons/no-preview.png") - self.assertContains(response, "3") - self.assertContains( - response, "/en-US/firefox/addon/a3615/?src=api") - self.assertContains( - response, - 'hash="sha256:3808b13ef8341378b9c8305ca648200954ee7dcd8dce09fef55f' - '2673458bc31f"') - - def test_addon_detail_json(self): - addon = Addon.objects.get(id=3615) - response = self.client.get( - '/en-US/firefox/api/%.1f/addon/3615?format=json' % 1.2) - data = json.loads(force_text(response.content)) - assert data['name'] == six.text_type(addon.name) - assert data['type'] == 'extension' - assert data['guid'] == addon.guid - assert data['version'] == '2.1.072' - assert data['status'] == 'public' - assert data['authors'] == ( - [{ - 'id': 55021, - 'name': u'55021 \u0627\u0644\u062a\u0637\u0628', - 'link': jinja_helpers.absolutify( - u'/en-US/firefox/user/55021/?src=api')}]) - assert data['summary'] == six.text_type(addon.summary) - assert data['description'] == ( - 'This extension integrates your browser with Delicious ' - '(http://delicious.com), the leading social bookmarking ' - 'service on the Web.') - assert data['icon'] == ( - '%s3/3615-32.png?modified=1275037317' % - jinja_helpers.user_media_url('addon_icons')) - assert data['compatible_apps'] == ( - [{'Firefox': {'max': '4.0', 'min': '2.0'}}]) - assert data['eula'] == six.text_type(addon.eula) - assert data['learnmore'] == ( - jinja_helpers.absolutify('/en-US/firefox/addon/a3615/?src=api')) - assert 'theme' not in data - - def test_theme_detail(self): - addon = Addon.objects.get(id=3615) - addon.update(type=amo.ADDON_PERSONA) - Persona.objects.create(persona_id=3, addon=addon) - response = self.client.get( - '/en-US/firefox/api/%.1f/addon/3615?format=json' % 1.2) - data = json.loads(force_text(response.content)) - assert data['id'] == 3615 - # `id` should be `addon_id`, not `persona_id` - assert data['theme']['id'] == '3615' - - def test_addon_license(self): - """Test for license information in response.""" - addon = Addon.objects.get(id=3615) - license = addon.current_version.license - license.name = 'My License' - license.url = 'someurl' - license.save() - api_url = ( - '/en-US/firefox/api/%.1f/addon/3615' % legacy_api.CURRENT_VERSION) - response = self.client.get(api_url) - doc = pq(response.content) - assert doc('license').length == 1 - assert doc('license name').length == 1 - assert doc('license url').length == 1 - assert doc('license name').text() == six.text_type(license.name) - assert ( - doc('license url').text() == jinja_helpers.absolutify(license.url)) - - license.url = '' - license.save() - addon.save() - response = self.client.get(api_url) - doc = pq(response.content) - license_url = addon.current_version.license_url() - assert ( - doc('license url').text() == jinja_helpers.absolutify(license_url)) - - license.delete() - response = self.client.get(api_url) - doc = pq(response.content) - assert doc('license').length == 0 - - def test_whitespace(self): - """Whitespace is apparently evil for learnmore and install.""" - r = make_call('addon/3615') - doc = pq(r.content) - learnmore = doc('learnmore')[0].text - assert learnmore == learnmore.strip() - - install = doc('install')[0].text - assert install == install.strip() - - def test_absolute_install_url(self): - response = make_call('addon/4664', version=1.2) - doc = pq(response.content) - url = doc('install').text() - expected = '%s/firefox/downloads/file' % settings.SITE_URL - assert url.startswith(expected), url - - def test_15_addon_detail(self): - """ - For an api>1.5 we need to verify we have: - # Contributions information, which is now just the contributions url, - # sent as the link to Meet the Developers - # Number of user reviews and link to view them - # Total downloads, weekly downloads, and latest daily user counts - # Add-on creation date - # Link to the developer's profile - # File size - """ - def urlparams(x, *args, **kwargs): - return jinja2.escape(jinja_helpers.urlparams(x, *args, **kwargs)) - - needles = ( - '', - '', - '', - 'https://patreon.com/blah', - '', - '', - '%s/en-US/firefox/addon/4664/reviews/?src=api' % settings.SITE_URL, - '1352192', - '13849', - '67075', - '' % settings.SITE_URL, - '', - 'preview position="0">', - 'TwitterBar places an icon in the address bar.', - 'full type="image/png">', - '', - ('Embrace hug love hug meow meow' - ''), - 'size="100352"', - ('http://www.chrisfinke.com/addons/twitterbar/' - ''), - 'http://www.chrisfinke.com/addons/twitterbar/') - - # For urls with several parameters, we need to use self.assertUrlEqual, - # as the parameters could be in random order. Dicts aren't ordered! - # We need to subtract 7 hours from the modified time since May 3, 2008 - # is during daylight savings time. - url_needles = { - "full": urlparams( - '{previews}full/20/20397.png'.format( - previews=jinja_helpers.user_media_url('previews')), - src='api', modified=1209834208 - 7 * 3600), - "thumbnail": urlparams( - '{previews}thumbs/20/20397.png'.format( - previews=jinja_helpers.user_media_url('previews')), - src='api', modified=1209834208 - 7 * 3600), - } - - response = make_call('addon/4664', version=1.5) - doc = pq(response.content) - - tags = { - 'created': ({'epoch': '1174109035'}, '2007-03-17T05:23:55Z'), - 'last_updated': ({'epoch': '1272301783'}, '2010-04-26T17:09:43Z')} - - for tag, v in tags.items(): - attrs, text = v - el = doc(tag) - for attr, val in attrs.items(): - assert el.attr(attr) == val - - assert el.text() == text - - for needle in needles: - self.assertContains(response, needle) - - for tag, needle in six.iteritems(url_needles): - url = doc(tag).text() - self.assertUrlEqual(url, needle) - - def test_slug(self): - Addon.objects.get(pk=5299).update(type=amo.ADDON_EXTENSION) - self.assertContains(make_call('addon/5299', version=1.5), - '%s' % - Addon.objects.get(pk=5299).slug) - - def test_is_featured(self): - self.assertContains(make_call('addon/5299', version=1.5), - '0') - c = CollectionAddon.objects.create( - addon=Addon.objects.get(id=5299), - collection=Collection.objects.create()) - FeaturedCollection.objects.create( - locale='ja', application=amo.FIREFOX.id, collection=c.collection) - for lang, app, result in [('ja', 'firefox', 1), - ('en-US', 'firefox', 0), - ('ja', 'android', 0)]: - self.assertContains(make_call('addon/5299', version=1.5, - lang=lang, app=app), - '%s' % result) - - def test_default_icon(self): - addon = Addon.objects.get(pk=5299) - addon.update(icon_type='') - self.assertContains(make_call('addon/5299'), '') - - def test_thumbnail_size(self): - addon = Addon.objects.get(pk=5299) - preview = Preview.objects.create(addon=addon) - preview.sizes = {'thumbnail': [200, 150]} - preview.save() - result = make_call('addon/5299', version=1.5) - self.assertContains(result, '') - self.assertContains( - result, '') - - def test_disabled_addon(self): - Addon.objects.get(pk=3615).update(disabled_by_user=True) - response = self.client.get('/en-US/firefox/api/%.1f/addon/3615' % - legacy_api.CURRENT_VERSION) - doc = pq(response.content) - assert doc[0].tag == 'error' - assert response.status_code == 404 - - def test_addon_with_no_listed_versions(self): - self.make_addon_unlisted(Addon.objects.get(pk=3615)) - response = self.client.get('/en-US/firefox/api/%.1f/addon/3615' % - legacy_api.CURRENT_VERSION) - doc = pq(response.content) - assert doc[0].tag == 'error' - assert response.status_code == 404 - - def test_cross_origin(self): - # Add-on details should allow cross-origin requests. - response = self.client.get('/en-US/firefox/api/%.1f/addon/3615' % - legacy_api.CURRENT_VERSION) - - assert response['Access-Control-Allow-Origin'] == '*' - assert response['Access-Control-Allow-Methods'] == 'GET' - - # Even those that are not found. - response = self.client.get('/en-US/firefox/api/%.1f/addon/999' % - legacy_api.CURRENT_VERSION) - - assert response['Access-Control-Allow-Origin'] == '*' - assert response['Access-Control-Allow-Methods'] == 'GET' - - -class ListTest(TestCase): - """Tests the list view with various urls.""" - fixtures = ['base/users', 'base/addon_3615', 'base/featured', - 'addons/featured', 'bandwagon/featured_collections', - 'base/collections'] - - def test_defaults(self): - """ - This tests the default settings for /list. - i.e. We should get 3 items by default. - """ - response = make_call('list') - self.assertContains(response, 'Theme""", 1) - - def test_persona_search_15(self): - response = make_call('list/recommended/9/1', version=1.5) - self.assertContains(response, """Theme""", 1) - - def test_limits(self): - """ - Assert /list/recommended/all/1 gets one item only. - """ - response = make_call('list/recommended/all/1') - self.assertContains(response, "") - - def test_average_daily_users(self): - """Verify that average daily users returns data in order.""" - r = make_call('list/by_adu', version=1.5) - doc = pq(r.content) - vals = [int(a.text) for a in doc("average_daily_users")] - sorted_vals = sorted(vals, reverse=True) - assert vals == sorted_vals - - def test_adu_no_personas(self): - """Verify that average daily users does not return Themes.""" - response = make_call('list/by_adu') - self.assertNotContains(response, """Theme""") - - def test_featured_no_personas(self): - """Verify that featured does not return Themes.""" - response = make_call('list/featured') - self.assertNotContains(response, """Theme""") - - def test_json(self): - """Verify that we get some json.""" - response = make_call('list/by_adu?format=json', version=1.5) - assert json.loads(force_text(response.content)) - - def test_unicode(self): - make_call(u'list/featured/all/10/Linux/3.7a2prexec\xb6\u0153\xec\xb2') - - -class AddonFilterTest(TestCase): - """Tests the addon_filter, including the various d2c cases.""" - fixtures = ['base/appversion'] - - def setUp(self): - super(AddonFilterTest, self).setUp() - # Start with 2 compatible add-ons. - self.addon1 = addon_factory(version_kw={'max_app_version': '5.0'}) - self.addon2 = addon_factory(version_kw={'max_app_version': '6.0'}) - self.addons = [self.addon1, self.addon2] - - def _defaults(self, **kwargs): - # Default args for addon_filter. - defaults = { - 'addons': self.addons, - 'addon_type': 'ALL', - 'limit': 0, - 'app': amo.FIREFOX, - 'platform': 'all', - 'version': '5.0', - 'compat_mode': 'strict', - 'shuffle': False, - } - defaults.update(kwargs) - return defaults - - def test_basic(self): - addons = addon_filter(**self._defaults()) - assert addons == self.addons - - def test_limit(self): - addons = addon_filter(**self._defaults(limit=1)) - assert addons == [self.addon1] - - def test_app_filter(self): - self.addon1.update(type=amo.ADDON_DICT) - addons = addon_filter( - **self._defaults(addon_type=str(amo.ADDON_EXTENSION))) - assert addons == [self.addon2] - - def test_platform_filter(self): - file = self.addon1.current_version.files.all()[0] - file.update(platform=amo.PLATFORM_WIN.id) - # Transformers don't know 'bout my files. - self.addons[0] = Addon.objects.get(pk=self.addons[0].pk) - addons = addon_filter( - **self._defaults(platform=amo.PLATFORM_LINUX.shortname)) - assert addons == [self.addon2] - - def test_version_filter_strict(self): - addons = addon_filter(**self._defaults(version='6.0')) - assert addons == [self.addon2] - - def test_version_filter_ignore(self): - addons = addon_filter(**self._defaults(version='6.0', - compat_mode='ignore')) - assert addons == self.addons - - def test_version_version_less_than_min(self): - # Ensure we filter out addons with a higher min than our app. - addon3 = addon_factory(version_kw={ - 'min_app_version': '12.0', 'max_app_version': '14.0'}) - addons = self.addons + [addon3] - addons = addon_filter(**self._defaults(addons=addons, version='11.0', - compat_mode='ignore')) - assert addons == self.addons - - def test_version_filter_normal_strict_opt_in(self): - # Ensure we filter out strict opt-in addons in normal mode. - addon3 = addon_factory(version_kw={'max_app_version': '7.0'}, - file_kw={'strict_compatibility': True}) - addons = self.addons + [addon3] - addons = addon_filter(**self._defaults(addons=addons, version='11.0', - compat_mode='normal')) - assert addons == self.addons - - def test_version_filter_normal_binary_components(self): - # Ensure we filter out strict opt-in addons in normal mode. - addon3 = addon_factory(version_kw={'max_app_version': '7.0'}, - file_kw={'binary_components': True}) - addons = self.addons + [addon3] - addons = addon_filter(**self._defaults(addons=addons, version='11.0', - compat_mode='normal')) - assert addons == self.addons - - def test_version_filter_normal_compat_override(self): - # Ensure we filter out strict opt-in addons in normal mode. - addon3 = addon_factory() - addons = self.addons + [addon3] - - # Add override for this add-on. - compat = CompatOverride.objects.create(guid='three', addon=addon3) - CompatOverrideRange.objects.create( - compat=compat, app=1, - min_version=addon3.current_version.version, max_version='*') - - addons = addon_filter(**self._defaults(addons=addons, version='11.0', - compat_mode='normal')) - assert addons == self.addons - - def test_locale_preferencing(self): - # Add-ons matching the current locale get prioritized. - addon3 = addon_factory() - addon3.description = {'de': 'Unst Unst'} - addon3.save() - - addons = self.addons + [addon3] - - translation.activate('de') - addons = addon_filter(**self._defaults(addons=addons)) - assert addons == [addon3] + self.addons - translation.deactivate() - - -class TestGuidSearch(TestCase): - fixtures = ('base/addon_6113', 'base/addon_3615') - # These are the guids for addon 6113 and 3615. - good = ('search/guid:{22870005-adef-4c9d-ae36-d0e1f2f27e5a},' - '{2fa4ed95-0317-4c6a-a74c-5f3e3912c1f9}') - - def setUp(self): - super(TestGuidSearch, self).setUp() - addon = Addon.objects.get(id=3615) - compat_override = CompatOverride.objects.create(guid=addon.guid) - app = list(addon.compatible_apps.keys())[0] - CompatOverrideRange.objects.create(compat=compat_override, app=app.id) - - def test_success(self): - response = make_call(self.good) - dom = pq(response.content) - assert set(['3615', '6113']) == ( - set([a.attrib['id'] for a in dom('addon')])) - - # Make sure the blocks are there. - assert ['3615'] == [a.attrib['id'] for a in dom('addon_compatibility')] - - @patch('waffle.switch_is_active', lambda x: True) - def test_api_caching_locale(self): - addon = Addon.objects.get(pk=3615) - addon.summary = {'en-US': 'Delicious', 'fr': 'Francais'} - addon.save() - - # This will prime the cache with the en-US version. - response = make_call(self.good) - self.assertContains(response, 'Delicious') - - # We should get back the fr version, not the en-US one. - response = make_call(self.good, lang='fr') - self.assertContains(response, 'Francais') - - def test_api_caching_app(self): - response = make_call(self.good) - assert b'en-US/firefox/addon/None/reviews/?src=api' in response.content - assert b'en-US/android/addon/None/reviews/' not in response.content - - response = make_call(self.good, app='android') - assert b'en-US/android/addon/None/reviews/?src=api' in response.content - assert b'en-US/firefox/addon/None/reviews/' not in response.content - - def test_xss(self): - addon_factory(guid='test@xss', name='') - response = make_call('search/guid:test@xss') - assert b'