Merge pull request #1437 from diox/convert-failurenotificationresource-to-drf

Convert FailureNotificationResource, StatusPayResource and PreparePayResource to DRF (bug 910602, bug 910612 and bug 910609)
This commit is contained in:
Mathieu Pillard 2013-11-26 05:19:59 -08:00
Родитель 014eb7a0e9 a4a74199f4
Коммит 64b4db5251
5 изменённых файлов: 111 добавлений и 122 удалений

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

@ -684,7 +684,7 @@ Transaction failure
**Response**
:status 202: Notification will be sent.
:statuscode 401: The API user is not authorized to report failures.
:statuscode 403: The API user is not authorized to report failures.
.. _CORS: https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS
.. _WebPay: https://github.com/mozilla/webpay

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

@ -127,9 +127,7 @@ def _prepare_pay(request, addon):
'Preparing JWT for: %s' % (addon.pk), severity=3)
if request.API:
url = reverse('api_dispatch_detail', kwargs={
'resource_name': 'status', 'api_name': 'webpay',
'uuid': uuid_})
url = reverse('webpay-status', kwargs={'uuid': uuid_})
else:
url = reverse('webpay.pay_status', args=[addon.app_slug, uuid_])
return {'webpayJWT': jwt_, 'contribStatusURL': url}

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

@ -2,13 +2,14 @@ import calendar
import time
from django.conf import settings
from django.conf.urls.defaults import url
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404
import commonware.log
import django_filters
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.generics import GenericAPIView
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
@ -23,99 +24,84 @@ from amo.utils import send_mail_jinja
from market.models import Price
from stats.models import Contribution
from mkt.api.authentication import (OAuthAuthentication,
OptionalOAuthAuthentication,
from mkt.api.authentication import (OptionalOAuthAuthentication,
RestAnonymousAuthentication,
SharedSecretAuthentication)
from mkt.api.authorization import (AnonymousReadOnlyAuthorization,
Authorization, OwnerAuthorization,
PermissionAuthorization)
from mkt.api.base import (CORSResource, CORSMixin, GenericObject, http_error,
MarketplaceModelResource, MarketplaceResource,
MarketplaceView)
RestOAuthAuthentication,
RestSharedSecretAuthentication)
from mkt.api.authorization import (AllowOwner, AnonymousReadOnlyAuthorization,
GroupPermission, PermissionAuthorization)
from mkt.api.base import (CORSResource, CORSMixin, MarketplaceModelResource,
MarketplaceResource, MarketplaceView)
from mkt.api.exceptions import AlreadyPurchased
from mkt.purchase.webpay import _prepare_pay, sign_webpay_jwt
from mkt.purchase.utils import payments_enabled
from mkt.webpay.forms import FailureForm, PrepareForm, ProductIconForm
from mkt.webpay.models import ProductIcon
from mkt.webpay.serializers import PriceSerializer
from . import tasks
log = commonware.log.getLogger('z.webpay')
class PreparePayResource(CORSResource, MarketplaceResource):
webpayJWT = fields.CharField(attribute='webpayJWT', readonly=True)
contribStatusURL = fields.CharField(attribute='contribStatusURL',
readonly=True)
class PreparePayView(CORSMixin, MarketplaceView, GenericAPIView):
authentication_classes = [RestOAuthAuthentication,
RestSharedSecretAuthentication]
permission_classes = [AllowAny]
cors_allowed_methods = ['post']
class Meta(MarketplaceResource.Meta):
always_return_data = True
authentication = (SharedSecretAuthentication(), OAuthAuthentication())
authorization = Authorization()
detail_allowed_methods = []
list_allowed_methods = ['post']
object_class = GenericObject
resource_name = 'prepare'
validation = CleanedDataFormValidation(form_class=PrepareForm)
def post(self, request, *args, **kwargs):
form = PrepareForm(request.DATA)
if not form.is_valid():
return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
app = form.cleaned_data['app']
def obj_create(self, bundle, request, **kwargs):
region = getattr(request, 'REGION', None)
app = bundle.data['app']
if region and region.id not in app.get_price_region_ids():
log.info('Region {0} is not in {1}'
.format(region.id, app.get_price_region_ids()))
if payments_enabled(request):
log.info('Flag not active')
raise http_error(http.HttpForbidden,
'Payments are limited and flag not enabled')
return Response('Payments are limited and flag not enabled',
status=status.HTTP_403_FORBIDDEN)
bundle.obj = GenericObject(_prepare_pay(request, bundle.data['app']))
return bundle
class StatusPayResource(CORSResource, MarketplaceModelResource):
class Meta(MarketplaceModelResource.Meta):
always_return_data = True
authentication = (SharedSecretAuthentication(), OAuthAuthentication())
authorization = OwnerAuthorization()
detail_allowed_methods = ['get']
queryset = Contribution.objects.filter(type=amo.CONTRIB_PURCHASE)
resource_name = 'status'
def obj_get(self, request=None, **kw):
try:
obj = super(StatusPayResource, self).obj_get(request=request, **kw)
except ObjectDoesNotExist:
data = _prepare_pay(request._request, app)
except AlreadyPurchased:
return Response({'reason': u'Already purchased app.'},
status=status.HTTP_409_CONFLICT)
return Response(data, status=status.HTTP_201_CREATED)
class StatusPayView(CORSMixin, MarketplaceView, GenericAPIView):
authentication_classes = [RestOAuthAuthentication,
RestSharedSecretAuthentication]
permission_classes = [AllowOwner]
cors_allowed_methods = ['get']
queryset = Contribution.objects.filter(type=amo.CONTRIB_PURCHASE)
lookup_field = 'uuid'
def get_object(self):
try:
obj = super(StatusPayView, self).get_object()
except Http404:
# Anything that's not correct will be raised as a 404 so that it's
# harder to iterate over contribution values.
log.info('Contribution not found')
return None
if not OwnerAuthorization().is_authorized(request, object=obj):
raise http_error(http.HttpForbidden,
'You are not an author of that app.')
if not obj.addon.has_purchased(request.amo_user):
if not obj.addon.has_purchased(self.request.amo_user):
log.info('Not in AddonPurchase table')
return None
return obj
def base_urls(self):
return [
url(r"^(?P<resource_name>%s)/(?P<uuid>[^/]+)/$" %
self._meta.resource_name,
self.wrap_view('dispatch_detail'),
name='api_dispatch_detail')
]
def full_dehydrate(self, bundle):
bundle.data = {'status': 'complete' if bundle.obj.id else 'incomplete'}
return bundle
def get(self, request, *args, **kwargs):
self.object = self.get_object()
data = {'status': 'complete' if self.object else 'incomplete'}
return Response(data)
class PriceFilter(django_filters.FilterSet):
@ -136,31 +122,30 @@ class PricesViewSet(MarketplaceView, CORSMixin, ListModelMixin,
filter_class = PriceFilter
class FailureNotificationResource(MarketplaceModelResource):
class FailureNotificationView(MarketplaceView, GenericAPIView):
authentication_classes = [RestOAuthAuthentication,
RestSharedSecretAuthentication]
permission_classes = [GroupPermission('Transaction', 'NotifyFailure')]
queryset = Contribution.objects.filter(uuid__isnull=False)
class Meta:
authentication = OAuthAuthentication()
authorization = PermissionAuthorization('Transaction', 'NotifyFailure')
detail_allowed_methods = ['patch']
queryset = Contribution.objects.filter(uuid__isnull=False)
resource_name = 'failure'
def obj_update(self, bundle, **kw):
form = FailureForm(bundle.data)
def patch(self, request, *args, **kwargs):
form = FailureForm(request.DATA)
if not form.is_valid():
raise self.form_errors(form)
return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
data = {'transaction_id': bundle.obj,
'transaction_url': absolutify(
urlparams(reverse('mkt.developers.transactions'),
transaction_id=bundle.obj.uuid)),
'url': form.cleaned_data['url'],
'retries': form.cleaned_data['attempts']}
owners = bundle.obj.addon.authors.values_list('email', flat=True)
obj = self.get_object()
data = {
'transaction_id': obj,
'transaction_url': absolutify(
urlparams(reverse('mkt.developers.transactions'),
transaction_id=obj.uuid)),
'url': form.cleaned_data['url'],
'retries': form.cleaned_data['attempts']}
owners = obj.addon.authors.values_list('email', flat=True)
send_mail_jinja('Payment notification failure.',
'webpay/failure.txt',
data, recipient_list=owners)
return bundle
return Response(status=status.HTTP_202_ACCEPTED)
class ProductIconResource(CORSResource, MarketplaceModelResource):

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

@ -25,20 +25,21 @@ from mkt.webpay.resources import PricesViewSet
from stats.models import Contribution
class TestPrepare(PurchaseTest, BaseOAuth):
class TestPrepare(PurchaseTest, RestOAuth):
fixtures = fixture('webapp_337141', 'user_2519', 'prices')
def setUp(self):
BaseOAuth.setUp(self, api_name='webpay')
RestOAuth.setUp(self) # Avoid calling PurchaseTest.setUp().
self.create_switch('marketplace')
self.list_url = list_url('prepare')
self.list_url = reverse('webpay-prepare')
self.user = UserProfile.objects.get(pk=2519)
def test_allowed(self):
self._allowed_verbs(self.list_url, ['post'])
def test_anon(self):
eq_(self.anon.post(self.list_url, data={}).status_code, 401)
res = self.anon.post(self.list_url, data=json.dumps({'app': 337141}))
eq_(res.status_code, 403)
def test_good(self):
self.setup_base()
@ -46,9 +47,8 @@ class TestPrepare(PurchaseTest, BaseOAuth):
res = self.client.post(self.list_url, data=json.dumps({'app': 337141}))
contribution = Contribution.objects.get()
eq_(res.status_code, 201)
eq_(res.json['contribStatusURL'], reverse('api_dispatch_detail',
kwargs={'api_name': 'webpay', 'resource_name': 'status',
'uuid': contribution.uuid}))
eq_(res.json['contribStatusURL'],
reverse('webpay-status', kwargs={'uuid': contribution.uuid}))
ok_(res.json['webpayJWT'])
@patch('mkt.webapps.models.Webapp.has_purchased')
@ -58,7 +58,7 @@ class TestPrepare(PurchaseTest, BaseOAuth):
self.setup_package()
res = self.client.post(self.list_url, data=json.dumps({'app': 337141}))
eq_(res.status_code, 409)
eq_(res.content, '{"reason": "Already purchased app."}')
eq_(res.json, {"reason": "Already purchased app."})
def _post(self):
return self.client.post(self.list_url,
@ -73,17 +73,16 @@ class TestPrepare(PurchaseTest, BaseOAuth):
eq_(self._post().status_code, 201)
class TestStatus(BaseOAuth):
class TestStatus(RestOAuth):
fixtures = fixture('webapp_337141', 'user_2519')
def setUp(self):
super(TestStatus, self).setUp(api_name='webpay')
super(TestStatus, self).setUp()
self.contribution = Contribution.objects.create(
addon_id=337141, user_id=2519, type=CONTRIB_PURCHASE,
uuid='some:uid')
self.get_url = ('api_dispatch_detail', {
'api_name': 'webpay', 'resource_name': 'status',
'uuid': self.contribution.uuid})
self.get_url = reverse('webpay-status',
kwargs={'uuid': self.contribution.uuid})
def test_allowed(self):
self._allowed_verbs(self.get_url, ['get'])
@ -111,6 +110,12 @@ class TestStatus(BaseOAuth):
eq_(res.status_code, 200, res.content)
eq_(res.json['status'], 'incomplete', res.content)
def test_not_owner(self):
userprofile2 = UserProfile.objects.get(pk=31337)
self.contribution.update(user=userprofile2)
res = self.client.get(self.get_url)
eq_(res.status_code, 403, res.content)
class TestPrices(RestOAuth):
@ -215,49 +220,47 @@ class TestPrices(RestOAuth):
eq_(res.json['localized'], {})
class TestNotification(BaseOAuth):
class TestNotification(RestOAuth):
fixtures = fixture('webapp_337141', 'user_2519')
def setUp(self):
super(TestNotification, self).setUp(api_name='webpay')
super(TestNotification, self).setUp()
self.grant_permission(self.profile, 'Transaction:NotifyFailure')
self.contribution = Contribution.objects.create(addon_id=337141,
uuid='sample:uuid')
self.list_url = ('api_dispatch_list', {'resource_name': 'failure'})
self.get_url = ['api_dispatch_detail',
{'resource_name': 'failure',
'pk': self.contribution.pk}]
self.get_url = reverse('webpay-failurenotification',
kwargs={'pk': self.contribution.pk})
self.data = {'url': 'https://someserver.com', 'attempts': 5}
def test_list_allowed(self):
self._allowed_verbs(self.get_url, ['patch'])
def test_notify(self):
url = 'https://someserver.com'
res = self.client.patch(self.get_url,
data=json.dumps({'url': url, 'attempts': 5}))
res = self.client.patch(self.get_url, data=json.dumps(self.data))
eq_(res.status_code, 202)
eq_(len(mail.outbox), 1)
msg = mail.outbox[0]
assert url in msg.body
assert self.data['url'] in msg.body
eq_(msg.recipients(), [u'steamcube@mozilla.com'])
def test_no_permission(self):
GroupUser.objects.filter(user=self.profile).delete()
res = self.client.patch(self.get_url, data=json.dumps({}))
eq_(res.status_code, 401)
res = self.client.patch(self.get_url, data=json.dumps(self.data))
eq_(res.status_code, 403)
def test_missing(self):
res = self.client.patch(self.get_url, data=json.dumps({}))
eq_(res.status_code, 400)
def test_not_there(self):
self.get_url[1]['pk'] += 1
res = self.client.patch(self.get_url, data=json.dumps({}))
self.get_url = reverse('webpay-failurenotification',
kwargs={'pk': self.contribution.pk + 42})
res = self.client.patch(self.get_url, data=json.dumps(self.data))
eq_(res.status_code, 404)
def test_no_uuid(self):
self.contribution.update(uuid=None)
res = self.client.patch(self.get_url, data=json.dumps({}))
res = self.client.patch(self.get_url, data=json.dumps(self.data))
eq_(res.status_code, 404)
@ -327,7 +330,7 @@ class TestSigCheck(TestCase):
with self.settings(APP_PURCHASE_SECRET=secret,
APP_PURCHASE_KEY=key,
APP_PURCHASE_AUD=aud):
res = self.client.post(reverse('webpay.sig_check'))
res = self.client.post(reverse('webpay-sig_check'))
eq_(res.status_code, 201, res)
data = json.loads(res.content)
req = jwt.decode(data['sig_check_jwt'].encode('ascii'), secret)

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

@ -3,16 +3,13 @@ from django.conf.urls import include, patterns, url
from rest_framework import routers
from tastypie.api import Api
from mkt.webpay.resources import (FailureNotificationResource,
PreparePayResource, PricesViewSet,
from mkt.webpay.resources import (FailureNotificationView,
PreparePayView, PricesViewSet,
ProductIconResource, sig_check,
StatusPayResource)
StatusPayView)
api = Api(api_name='webpay')
api.register(FailureNotificationResource())
api.register(ProductIconResource())
api.register(PreparePayResource())
api.register(StatusPayResource())
api_router = routers.SimpleRouter()
api_router.register(r'prices', PricesViewSet)
@ -20,5 +17,11 @@ api_router.register(r'prices', PricesViewSet)
urlpatterns = patterns('',
url(r'^', include(api.urls)),
url(r'^webpay/', include(api_router.urls)),
url(r'^webpay/sig_check/$', sig_check, name='webpay.sig_check')
url(r'^webpay/status/(?P<uuid>[^/]+)/', StatusPayView.as_view(),
name='webpay-status'),
url(r'^webpay/prepare/', PreparePayView.as_view(),
name='webpay-prepare'),
url(r'^webpay/failure/(?P<pk>\d+)/', FailureNotificationView.as_view(),
name='webpay-failurenotification'),
url(r'^webpay/sig_check/$', sig_check, name='webpay-sig_check')
)