convert ReceiptResource (bug 910577)
This commit is contained in:
Родитель
e579423fdc
Коммит
b195d3952b
|
@ -1,3 +1,4 @@
|
|||
import functools
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
@ -12,6 +13,7 @@ from django.db.models.sql import EmptyResultSet
|
|||
from django.http import HttpResponseNotFound
|
||||
|
||||
import commonware.log
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.routers import Route, SimpleRouter
|
||||
from rest_framework.relations import HyperlinkedRelatedField
|
||||
|
@ -569,6 +571,16 @@ class CORSMixin(object):
|
|||
request, response, *args, **kwargs)
|
||||
|
||||
|
||||
def cors_api_view(methods):
|
||||
def decorator(f):
|
||||
@api_view(methods)
|
||||
@functools.wraps(f)
|
||||
def wrapped(request):
|
||||
request._request.CORS = methods
|
||||
return f(request)
|
||||
return wrapped
|
||||
return decorator
|
||||
|
||||
class SlugOrIdMixin(object):
|
||||
"""
|
||||
Because the `SlugRouter` is overkill. If the name of your
|
||||
|
|
|
@ -6,7 +6,7 @@ import commonware.log
|
|||
import waffle
|
||||
from celery_tasktree import TaskTree
|
||||
import raven.base
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.decorators import permission_classes
|
||||
from rest_framework import generics
|
||||
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
@ -40,13 +40,12 @@ from market.models import AddonPremium, Price, PriceCurrency
|
|||
from mkt.api.authentication import (SharedSecretAuthentication,
|
||||
OptionalOAuthAuthentication,
|
||||
RestOAuthAuthentication)
|
||||
from mkt.api.authorization import (AllowAppOwner, AllowReviewerReadOnly,
|
||||
AppOwnerAuthorization, GroupPermission,
|
||||
OwnerAuthorization)
|
||||
from mkt.api.base import (CORSMixin, CORSResource, http_error,
|
||||
MarketplaceModelResource, MarketplaceResource,
|
||||
SlugOrIdMixin)
|
||||
from mkt.api.forms import (CategoryForm, DeviceTypeForm, UploadForm)
|
||||
from mkt.api.authorization import (AllowAppOwner, AppOwnerAuthorization,
|
||||
GroupPermission, OwnerAuthorization)
|
||||
from mkt.api.base import (cors_api_view, CORSMixin, CORSResource,
|
||||
http_error, MarketplaceModelResource,
|
||||
MarketplaceResource, SlugOrIdMixin)
|
||||
from mkt.api.forms import CategoryForm, DeviceTypeForm, UploadForm
|
||||
from mkt.api.http import HttpLegallyUnavailable
|
||||
from mkt.api.serializers import CarrierSerializer, RegionSerializer
|
||||
from mkt.carriers import CARRIER_MAP, CARRIERS
|
||||
|
@ -359,14 +358,13 @@ def get_settings():
|
|||
return dict([k, safe[k]] for k in _settings)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@cors_api_view(['GET'])
|
||||
@permission_classes([AllowAny])
|
||||
def site_config(request):
|
||||
"""
|
||||
A resource that is designed to be exposed externally and contains
|
||||
settings or waffle flags that might be relevant to the client app.
|
||||
"""
|
||||
request._request.CORS = ['GET']
|
||||
return Response({
|
||||
# This is the git commit on IT servers.
|
||||
'version': getattr(settings, 'BUILD_ID_JS', ''),
|
||||
|
@ -398,7 +396,7 @@ class CarrierViewSet(RegionViewSet):
|
|||
return CARRIER_MAP.get(self.kwargs['pk'], None)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@cors_api_view(['POST'])
|
||||
@permission_classes([AllowAny])
|
||||
def error_reporter(request):
|
||||
request._request.CORS = ['POST']
|
||||
|
|
|
@ -9,6 +9,10 @@ from mock import patch, Mock
|
|||
from nose.tools import eq_, ok_
|
||||
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.decorators import (authentication_classes,
|
||||
permission_classes)
|
||||
from rest_framework.response import Response
|
||||
|
||||
from tastypie import http
|
||||
from tastypie.authentication import Authentication
|
||||
from tastypie.authorization import Authorization
|
||||
|
@ -18,8 +22,8 @@ from test_utils import RequestFactory
|
|||
|
||||
from access.middleware import ACLMiddleware
|
||||
from amo.tests import TestCase
|
||||
from mkt.api.base import (AppViewSet, CompatRelatedField, CORSResource,
|
||||
handle_500, MarketplaceResource)
|
||||
from mkt.api.base import (AppViewSet, cors_api_view, CompatRelatedField,
|
||||
CORSResource, handle_500, MarketplaceResource)
|
||||
from mkt.api.http import HttpTooManyRequests
|
||||
from mkt.api.serializers import Serializer
|
||||
from mkt.receipts.tests.test_views import RawRequestFactory
|
||||
|
@ -229,6 +233,17 @@ class TestCORSResource(TestCase):
|
|||
UnfilteredCORS().method_check(request, allowed=['get'])
|
||||
eq_(request.CORS, ['get'])
|
||||
|
||||
class TestCORSWrapper(TestCase):
|
||||
def test_cors(self):
|
||||
@cors_api_view(['GET', 'PATCH'])
|
||||
@authentication_classes([])
|
||||
@permission_classes([])
|
||||
def foo(request):
|
||||
return Response()
|
||||
request = RequestFactory().get('/')
|
||||
r = foo(request)
|
||||
eq_(request.CORS, ['GET', 'PATCH'])
|
||||
|
||||
|
||||
class Form(forms.Form):
|
||||
app = forms.ChoiceField(choices=(('valid', 'valid'),))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
import commonware.log
|
||||
from rest_framework.decorators import (api_view, authentication_classes,
|
||||
from rest_framework.decorators import (authentication_classes,
|
||||
parser_classes, permission_classes)
|
||||
from rest_framework.parsers import FormParser, JSONParser
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
@ -9,6 +9,7 @@ from rest_framework.response import Response
|
|||
|
||||
from mkt.api.authentication import (RestOAuthAuthentication,
|
||||
RestSharedSecretAuthentication)
|
||||
from mkt.api.base import cors_api_view
|
||||
from mkt.constants.apps import INSTALL_TYPE_USER
|
||||
from mkt.installs.forms import InstallForm
|
||||
from mkt.installs.utils import install_type, record
|
||||
|
@ -17,13 +18,12 @@ from mkt.webapps.models import Installed
|
|||
log = commonware.log.getLogger('z.api')
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@cors_api_view(['POST'])
|
||||
@authentication_classes([RestOAuthAuthentication,
|
||||
RestSharedSecretAuthentication])
|
||||
@parser_classes([JSONParser, FormParser])
|
||||
@permission_classes([AllowAny])
|
||||
def install(request):
|
||||
request._request.CORS = ['POST']
|
||||
form = InstallForm(request.DATA, request=request)
|
||||
|
||||
if form.is_valid():
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import commonware.log
|
||||
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.decorators import (authentication_classes,
|
||||
permission_classes)
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from tastypie import http
|
||||
from tastypie.authorization import Authorization
|
||||
from tastypie.validation import CleanedDataFormValidation
|
||||
|
||||
|
@ -12,11 +12,10 @@ from tastypie.validation import CleanedDataFormValidation
|
|||
from constants.payments import CONTRIB_NO_CHARGE
|
||||
from lib.cef_loggers import receipt_cef
|
||||
from market.models import AddonPurchase
|
||||
from mkt.api.authentication import (OAuthAuthentication,
|
||||
from mkt.api.authentication import (RestOAuthAuthentication,
|
||||
OptionalOAuthAuthentication,
|
||||
SharedSecretAuthentication)
|
||||
from mkt.api.base import CORSResource, http_error, MarketplaceResource
|
||||
from mkt.api.http import HttpPaymentRequired
|
||||
RestSharedSecretAuthentication)
|
||||
from mkt.api.base import cors_api_view, CORSResource, MarketplaceResource
|
||||
from mkt.constants import apps
|
||||
from mkt.installs.utils import install_type, record
|
||||
from mkt.receipts.forms import ReceiptForm, TestInstall
|
||||
|
@ -28,69 +27,59 @@ from mkt.webapps.models import Installed
|
|||
log = commonware.log.getLogger('z.receipt')
|
||||
|
||||
|
||||
class ReceiptResource(CORSResource, MarketplaceResource):
|
||||
@cors_api_view(['POST'])
|
||||
@authentication_classes([RestOAuthAuthentication,
|
||||
RestSharedSecretAuthentication])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def install(request):
|
||||
form = ReceiptForm(request.DATA)
|
||||
|
||||
class Meta(MarketplaceResource.Meta):
|
||||
always_return_data = True
|
||||
authentication = (SharedSecretAuthentication(),
|
||||
OAuthAuthentication())
|
||||
authorization = Authorization()
|
||||
detail_allowed_methods = []
|
||||
list_allowed_methods = ['post']
|
||||
object_class = dict
|
||||
resource_name = 'install'
|
||||
if not form.is_valid():
|
||||
return Response({'error_message': form.errors}, status=400)
|
||||
|
||||
def obj_create(self, bundle, request=None, **kwargs):
|
||||
bundle.data['receipt'] = self.handle(bundle, request=request, **kwargs)
|
||||
record(request, bundle.obj)
|
||||
return bundle
|
||||
|
||||
def handle(self, bundle, request, **kwargs):
|
||||
form = ReceiptForm(bundle.data)
|
||||
|
||||
if not form.is_valid():
|
||||
raise self.form_errors(form)
|
||||
|
||||
bundle.obj = form.cleaned_data['app']
|
||||
type_ = install_type(request, bundle.obj)
|
||||
|
||||
if type_ == apps.INSTALL_TYPE_DEVELOPER:
|
||||
return self.record(bundle, request, apps.INSTALL_TYPE_DEVELOPER)
|
||||
obj = form.cleaned_data['app']
|
||||
type_ = install_type(request, obj)
|
||||
|
||||
if type_ == apps.INSTALL_TYPE_DEVELOPER:
|
||||
receipt = install_record(obj, request,
|
||||
apps.INSTALL_TYPE_DEVELOPER)
|
||||
else:
|
||||
# The app must be public and if its a premium app, you
|
||||
# must have purchased it.
|
||||
if not bundle.obj.is_public():
|
||||
log.info('App not public: %s' % bundle.obj.pk)
|
||||
raise http_error(http.HttpForbidden, 'App not public.')
|
||||
if not obj.is_public():
|
||||
log.info('App not public: %s' % obj.pk)
|
||||
return Response('App not public.', status=403)
|
||||
|
||||
if (bundle.obj.is_premium() and
|
||||
not bundle.obj.has_purchased(request.amo_user)):
|
||||
if (obj.is_premium() and
|
||||
not obj.has_purchased(request.amo_user)):
|
||||
# Apps that are premium but have no charge will get an
|
||||
# automatic purchase record created. This will ensure that
|
||||
# the receipt will work into the future if the price changes.
|
||||
if bundle.obj.premium and not bundle.obj.premium.price.price:
|
||||
log.info('Create purchase record: {0}'.format(bundle.obj.pk))
|
||||
AddonPurchase.objects.get_or_create(addon=bundle.obj,
|
||||
if obj.premium and not obj.premium.price.price:
|
||||
log.info('Create purchase record: {0}'.format(obj.pk))
|
||||
AddonPurchase.objects.get_or_create(addon=obj,
|
||||
user=request.amo_user, type=CONTRIB_NO_CHARGE)
|
||||
else:
|
||||
log.info('App not purchased: %s' % bundle.obj.pk)
|
||||
raise http_error(HttpPaymentRequired, 'You have not purchased this app.')
|
||||
log.info('App not purchased: %s' % obj.pk)
|
||||
return Response('You have not purchased this app.', status=402)
|
||||
receipt = install_record(obj, request, type_)
|
||||
record(request, obj)
|
||||
return Response({'receipt': receipt}, status=201)
|
||||
|
||||
return self.record(bundle, request, type_)
|
||||
|
||||
def record(self, bundle, request, install_type):
|
||||
# Generate or re-use an existing install record.
|
||||
installed, created = Installed.objects.get_or_create(
|
||||
addon=bundle.obj, user=request.user.get_profile(),
|
||||
install_type=install_type)
|
||||
def install_record(obj, request, install_type):
|
||||
# Generate or re-use an existing install record.
|
||||
installed, created = Installed.objects.get_or_create(
|
||||
addon=obj, user=request.user.get_profile(),
|
||||
install_type=install_type)
|
||||
|
||||
log.info('Installed record %s: %s' % (
|
||||
'created' if created else 're-used',
|
||||
bundle.obj.pk))
|
||||
log.info('Installed record %s: %s' % (
|
||||
'created' if created else 're-used',
|
||||
obj.pk))
|
||||
|
||||
log.info('Creating receipt: %s' % bundle.obj.pk)
|
||||
receipt_cef.log(request, bundle.obj, 'sign', 'Receipt signing')
|
||||
return create_receipt(installed)
|
||||
log.info('Creating receipt: %s' % obj.pk)
|
||||
receipt_cef.log(request._request, obj, 'sign', 'Receipt signing')
|
||||
return create_receipt(installed)
|
||||
|
||||
|
||||
class TestReceiptResource(CORSResource, MarketplaceResource):
|
||||
|
@ -112,7 +101,7 @@ class TestReceiptResource(CORSResource, MarketplaceResource):
|
|||
return bundle
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@cors_api_view(['POST'])
|
||||
@permission_classes((AllowAny,))
|
||||
def reissue(request):
|
||||
# This is just a place holder for reissue that will hopefully return
|
||||
|
|
|
@ -17,27 +17,26 @@ from addons.models import Addon, AddonUser
|
|||
from constants.payments import CONTRIB_NO_CHARGE
|
||||
from devhub.models import AppLog
|
||||
from mkt.api.base import list_url
|
||||
from mkt.api.tests.test_oauth import BaseOAuth
|
||||
from mkt.api.tests.test_oauth import BaseOAuth, RestOAuth
|
||||
from mkt.constants import apps
|
||||
from mkt.receipts.api import HttpPaymentRequired, ReceiptResource
|
||||
from mkt.site.fixtures import fixture
|
||||
from users.models import UserProfile
|
||||
|
||||
|
||||
@mock.patch.object(settings, 'WEBAPPS_RECEIPT_KEY',
|
||||
amo.tests.AMOPaths.sample_key())
|
||||
class TestAPI(BaseOAuth):
|
||||
class TestAPI(RestOAuth):
|
||||
fixtures = fixture('user_2519', 'webapp_337141')
|
||||
|
||||
def setUp(self):
|
||||
super(TestAPI, self).setUp(api_name='receipts')
|
||||
super(TestAPI, self).setUp()
|
||||
self.addon = Addon.objects.get(pk=337141)
|
||||
self.url = list_url('install')
|
||||
self.url = reverse('receipt.install')
|
||||
self.data = json.dumps({'app': self.addon.pk})
|
||||
self.profile = self.user.get_profile()
|
||||
|
||||
def test_has_cors(self):
|
||||
self.assertCORS(self.client.get(self.url), 'post')
|
||||
self.assertCORS(self.client.post(self.url), 'post')
|
||||
|
||||
def post(self, anon=False):
|
||||
client = self.client if not anon else self.anon
|
||||
|
@ -53,7 +52,7 @@ class TestAPI(BaseOAuth):
|
|||
|
||||
def test_record_logged_out(self):
|
||||
res = self.post(anon=True)
|
||||
eq_(res.status_code, 401)
|
||||
eq_(res.status_code, 403)
|
||||
|
||||
@mock.patch('mkt.receipts.api.receipt_cef.log')
|
||||
def test_cef_logs(self, cef):
|
||||
|
@ -119,69 +118,63 @@ class TestDevhubAPI(BaseOAuth):
|
|||
|
||||
@mock.patch.object(settings, 'WEBAPPS_RECEIPT_KEY',
|
||||
amo.tests.AMOPaths.sample_key())
|
||||
class TestReceipt(amo.tests.TestCase):
|
||||
class TestReceipt(RestOAuth):
|
||||
fixtures = fixture('user_2519', 'webapp_337141')
|
||||
|
||||
def setUp(self):
|
||||
super(TestReceipt, self).setUp()
|
||||
self.addon = Addon.objects.get(pk=337141)
|
||||
self.bundle = Bundle(data={'app': self.addon.pk})
|
||||
self.data = json.dumps({'app': self.addon.pk})
|
||||
self.profile = UserProfile.objects.get(pk=2519)
|
||||
self.resource = ReceiptResource()
|
||||
self.url = reverse('receipt.install')
|
||||
|
||||
def get_request(self, profile):
|
||||
request = RequestFactory().post('/')
|
||||
if not profile:
|
||||
request.user = AnonymousUser()
|
||||
else:
|
||||
request.user = profile.user
|
||||
request.amo_user = profile
|
||||
return request
|
||||
|
||||
def handle(self, profile):
|
||||
return self.resource.handle(self.bundle, self.get_request(profile))
|
||||
def post(self, anon=False):
|
||||
client = self.client if not anon else self.anon
|
||||
return client.post(self.url, data=self.data)
|
||||
|
||||
def test_pending_free_for_developer(self):
|
||||
AddonUser.objects.create(addon=self.addon, user=self.profile)
|
||||
self.addon.update(status=amo.STATUS_PENDING)
|
||||
ok_(self.handle(self.profile))
|
||||
eq_(self.post().status_code, 201)
|
||||
|
||||
def test_pending_free_for_anonymous(self):
|
||||
self.addon.update(status=amo.STATUS_PENDING)
|
||||
with self.assertImmediate(http.HttpForbidden):
|
||||
ok_(self.handle(None))
|
||||
r = self.anon.post(self.url)
|
||||
eq_(self.post(anon=True).status_code, 403)
|
||||
|
||||
|
||||
def test_pending_paid_for_developer(self):
|
||||
AddonUser.objects.create(addon=self.addon, user=self.profile)
|
||||
self.addon.update(status=amo.STATUS_PENDING,
|
||||
premium_type=amo.ADDON_PREMIUM)
|
||||
ok_(self.handle(self.profile))
|
||||
eq_(self.post().status_code, 201)
|
||||
eq_(self.profile.installed_set.all()[0].install_type,
|
||||
apps.INSTALL_TYPE_DEVELOPER)
|
||||
|
||||
def test_pending_paid_for_anonymous(self):
|
||||
self.addon.update(status=amo.STATUS_PENDING,
|
||||
premium_type=amo.ADDON_PREMIUM)
|
||||
with self.assertImmediate(http.HttpForbidden):
|
||||
ok_(self.handle(None))
|
||||
eq_(self.post(anon=True).status_code, 403)
|
||||
|
||||
def test_not_record_addon(self):
|
||||
self.addon.update(type=amo.ADDON_EXTENSION)
|
||||
with self.assertImmediate(http.HttpBadRequest):
|
||||
ok_(self.handle(self.profile))
|
||||
r = self.client.post(self.url)
|
||||
eq_(r.status_code, 400)
|
||||
|
||||
@mock.patch('mkt.webapps.models.Webapp.has_purchased')
|
||||
def test_paid(self, has_purchased):
|
||||
has_purchased.return_value = True
|
||||
self.addon.update(premium_type=amo.ADDON_PREMIUM)
|
||||
ok_(self.handle(self.profile))
|
||||
r = self.post()
|
||||
eq_(r.status_code, 201)
|
||||
|
||||
def test_own_payments(self):
|
||||
self.addon.update(premium_type=amo.ADDON_OTHER_INAPP)
|
||||
ok_(self.handle(self.profile))
|
||||
eq_(self.post().status_code, 201)
|
||||
|
||||
def test_no_charge(self):
|
||||
self.make_premium(self.addon, '0.00')
|
||||
ok_(self.handle(self.profile))
|
||||
eq_(self.post().status_code, 201)
|
||||
eq_(self.profile.installed_set.all()[0].install_type,
|
||||
apps.INSTALL_TYPE_USER)
|
||||
eq_(self.profile.addonpurchase_set.all()[0].type,
|
||||
|
@ -191,26 +184,26 @@ class TestReceipt(amo.tests.TestCase):
|
|||
def test_not_paid(self, has_purchased):
|
||||
has_purchased.return_value = False
|
||||
self.addon.update(premium_type=amo.ADDON_PREMIUM)
|
||||
with self.assertImmediate(HttpPaymentRequired):
|
||||
ok_(self.handle(self.profile))
|
||||
eq_(self.post().status_code, 402)
|
||||
|
||||
@mock.patch('mkt.receipts.api.receipt_cef.log')
|
||||
def test_record_install(self, cef):
|
||||
ok_(self.handle(self.profile))
|
||||
self.post()
|
||||
installed = self.profile.installed_set.all()
|
||||
eq_(len(installed), 1)
|
||||
eq_(installed[0].install_type, apps.INSTALL_TYPE_USER)
|
||||
|
||||
@mock.patch('mkt.receipts.api.receipt_cef.log')
|
||||
def test_record_multiple_installs(self, cef):
|
||||
ok_(self.handle(self.profile))
|
||||
ok_(self.handle(self.profile))
|
||||
self.post()
|
||||
r = self.post()
|
||||
eq_(r.status_code, 201)
|
||||
eq_(self.profile.installed_set.count(), 1)
|
||||
|
||||
@mock.patch('mkt.receipts.api.receipt_cef.log')
|
||||
def test_record_receipt(self, cef):
|
||||
res = self.handle(self.profile)
|
||||
ok_(Receipt(res).receipt_decoded())
|
||||
r = self.post()
|
||||
ok_(Receipt(r.data['receipt']).receipt_decoded())
|
||||
|
||||
|
||||
class TestReissue(amo.tests.TestCase):
|
||||
|
|
|
@ -6,7 +6,6 @@ import amo
|
|||
from . import api, views
|
||||
|
||||
receipt = Api(api_name='receipts')
|
||||
receipt.register(api.ReceiptResource())
|
||||
receipt.register(api.TestReceiptResource())
|
||||
|
||||
# Note: this URL is embedded in receipts, if you change the URL, make sure
|
||||
|
@ -27,6 +26,7 @@ receipt_patterns = patterns('',
|
|||
|
||||
receipt_api_patterns = patterns('',
|
||||
url(r'^', include(receipt.urls)),
|
||||
url(r'^receipts/install/', api.install, name='receipt.install'),
|
||||
url(r'^receipts/reissue/', api.reissue, name='receipt.reissue')
|
||||
)
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче