convert ReceiptResource (bug 910577)

This commit is contained in:
Allen Short 2013-11-14 16:07:22 -08:00
Родитель e579423fdc
Коммит b195d3952b
7 изменённых файлов: 119 добавлений и 112 удалений

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

@ -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')
)