Skip to content
This repository was archived by the owner on Mar 15, 2018. It is now read-only.

Commit 80fe70a

Browse files
author
Andy McKay
committed
Merge branch '699486'
2 parents 12f93d4 + 7a1b542 commit 80fe70a

File tree

13 files changed

+401
-145
lines changed

13 files changed

+401
-145
lines changed

apps/market/tests/test_models.py

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -109,80 +109,6 @@ def test_transformer(self):
109109
eq_(prices[0].get_price_locale(), u'$0.99')
110110

111111

112-
class ReceiptCase(amo.tests.TestCase):
113-
fixtures = ['base/addon_3615', 'base/users']
114-
115-
def setUp(self):
116-
self.addon = Addon.objects.get(pk=3615)
117-
self.addon.update(type=amo.ADDON_WEBAPP,
118-
manifest_url='http://cbc.ca/manifest')
119-
self.addon = Addon.objects.get(pk=3615)
120-
self.user = UserProfile.objects.get(pk=999)
121-
self.url = reverse('api.market.verify', args=[self.addon.slug])
122-
123-
124-
@mock.patch('addons.models.Addon.is_premium', lambda x: True)
125-
class TestAddonReceipt(ReceiptCase):
126-
127-
def test_anonymous(self):
128-
eq_(self.client.get(self.url).status_code, 302)
129-
130-
def test_wrong_type(self):
131-
self.client.login(username='regular@mozilla.com', password='password')
132-
self.addon.update(type=amo.ADDON_EXTENSION)
133-
res = self.client.get(self.url)
134-
eq_(res.status_code, 400)
135-
136-
def test_logged_in(self):
137-
self.addon.update(premium_type=amo.ADDON_PREMIUM)
138-
self.client.login(username='regular@mozilla.com', password='password')
139-
res = self.client.get(self.url)
140-
eq_(res.status_code, 200)
141-
eq_(json.loads(res.content)['status'], 'invalid')
142-
143-
def test_logged_in_ok(self):
144-
self.addon.update(premium_type=amo.ADDON_PREMIUM)
145-
self.client.login(username='regular@mozilla.com', password='password')
146-
self.addon.addonpurchase_set.create(user=self.user)
147-
res = self.client.get(self.url)
148-
eq_(res.status_code, 200)
149-
eq_(json.loads(res.content)['status'], 'ok')
150-
151-
def test_logged_in_other(self):
152-
self.addon.update(premium_type=amo.ADDON_PREMIUM)
153-
self.client.login(username='admin@mozilla.com', password='password')
154-
self.addon.addonpurchase_set.create(user=self.user)
155-
res = self.client.get(self.url)
156-
eq_(res.status_code, 200)
157-
eq_(json.loads(res.content)['status'], 'invalid')
158-
159-
def test_user_not_purchased(self):
160-
self.addon.update(premium_type=amo.ADDON_PREMIUM)
161-
eq_(list(self.user.purchase_ids()), [])
162-
163-
def test_user_purchased(self):
164-
self.addon.update(premium_type=amo.ADDON_PREMIUM)
165-
self.addon.addonpurchase_set.create(user=self.user)
166-
eq_(list(self.user.purchase_ids()), [3615L])
167-
168-
169-
@mock.patch('addons.models.Addon.is_premium', lambda x: False)
170-
class TestAddonReceiptFree(ReceiptCase):
171-
172-
def test_free_install(self):
173-
self.client.login(username='regular@mozilla.com', password='password')
174-
self.addon.get_or_create_install(self.user)
175-
res = self.client.get(self.url)
176-
eq_(res.status_code, 200)
177-
eq_(json.loads(res.content)['status'], 'ok')
178-
179-
def test_free_no_install(self):
180-
self.client.login(username='regular@mozilla.com', password='password')
181-
res = self.client.get(self.url)
182-
eq_(res.status_code, 200)
183-
eq_(json.loads(res.content)['status'], 'invalid')
184-
185-
186112
class TestContribution(amo.tests.TestCase):
187113
fixtures = ['base/addon_3615', 'base/users']
188114

apps/market/urls.py

Lines changed: 0 additions & 10 deletions
This file was deleted.

apps/market/views.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

apps/webapps/models.py

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from django.conf import settings
77
from django.db import models
88
from django.dispatch import receiver
9-
from django.utils.http import urlencode
109

1110
import commonware.log
1211

@@ -15,6 +14,7 @@
1514
from amo.helpers import absolutify
1615
import amo.models
1716
from amo.urlresolvers import reverse
17+
from amo.utils import memoize
1818
from addons import query
1919
from addons.models import (Addon, clear_name_table, delete_search_index,
2020
update_name_table, update_search_index)
@@ -135,47 +135,56 @@ class Installed(amo.models.ModelBase):
135135
"""Track WebApp installations."""
136136
addon = models.ForeignKey('addons.Addon', related_name='installed')
137137
user = models.ForeignKey('users.UserProfile')
138-
receipt = models.TextField(default='')
138+
# This is the email used by user at the time of installation.
139+
# It might be the real email, or a pseudonym, this is what will be going
140+
# into the receipt for verification later.
141+
email = models.CharField(max_length=255, db_index=True)
142+
# Because the addon could change between free and premium,
143+
# we need to store the state at time of install here.
144+
premium_type = models.PositiveIntegerField(
145+
choices=amo.ADDON_PREMIUM_TYPES.items(),
146+
null=True, default=None)
139147

140148
class Meta:
141149
db_table = 'users_install'
142150
unique_together = ('addon', 'user')
143151

144-
def create_receipt(self):
145-
verify = reverse('api.market.verify', args=[self.addon.pk])
146-
detail = reverse('users.purchases.receipt', args=[self.addon.pk])
147-
hsh = self.addon.get_watermark_hash(self.user)
148-
url = urlencode({amo.WATERMARK_KEY: self.user.email,
149-
amo.WATERMARK_KEY_HASH: hsh})
150-
verify = '%s?%s' % (verify, url)
151-
receipt = dict(typ='purchase-receipt',
152-
product=self.addon.origin,
153-
user={'type': 'email',
154-
'value': self.user.email},
155-
iss=settings.SITE_URL,
156-
nbf=time.time(),
157-
iat=time.time(),
158-
detail=absolutify(detail),
159-
verify=absolutify(verify))
160-
self.receipt = jwt.encode(receipt, get_key())
152+
@property
153+
def receipt(self):
154+
if self.addon.is_webapp():
155+
return create_receipt(self.pk)
156+
return ''
157+
158+
159+
@receiver(models.signals.post_save, sender=Installed)
160+
def add_email(sender, **kw):
161+
if not kw.get('raw'):
162+
install = kw['instance']
163+
if not install.email and install.premium_type == None:
164+
install.email = install.user.email
165+
install.premium_type = install.addon.premium_type
166+
install.save()
167+
168+
169+
@memoize(prefix='create-receipt', time=60 * 10)
170+
def create_receipt(installed_pk):
171+
installed = Installed.objects.get(pk=installed_pk)
172+
verify = '%s%s' % (settings.WEBAPPS_RECEIPT_URL, installed.addon.pk)
173+
detail = reverse('users.purchases.receipt', args=[installed.addon.pk])
174+
receipt = dict(typ='purchase-receipt',
175+
product=installed.addon.origin,
176+
user={'type': 'email',
177+
'value': installed.email},
178+
iss=settings.SITE_URL,
179+
nbf=time.mktime(installed.created.timetuple()),
180+
iat=time.time(),
181+
detail=absolutify(detail),
182+
verify=absolutify(verify))
183+
return jwt.encode(receipt, get_key(), u'RS512')
161184

162185

163186
def get_key():
164187
"""Return a key for using with encode."""
165188
return jwt.rsa_load(settings.WEBAPPS_RECEIPT_KEY)
166189

167190

168-
@receiver(models.signals.post_save, sender=Installed,
169-
dispatch_uid='create_receipt')
170-
def create_receipt(sender, instance, **kw):
171-
"""
172-
When something gets installed, see if we need to create a receipt.
173-
"""
174-
if (kw.get('raw') or instance.addon.type != amo.ADDON_WEBAPP
175-
or instance.receipt):
176-
return
177-
178-
log.debug('Creating receipt for: addon %s, user %s'
179-
% (instance.addon.pk, instance.user.pk))
180-
instance.create_receipt()
181-
instance.save()

apps/webapps/tests/test_models.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def test_reviewed(self):
8585

8686
def test_unreviewed(self):
8787
for status in amo.UNREVIEWED_STATUSES:
88-
w = Webapp.objects.create(status=status)
88+
Webapp.objects.create(status=status)
8989
self.reviewed_eq()
9090
Webapp.objects.all().delete()
9191

@@ -187,7 +187,7 @@ def test_has_installed(self):
187187

188188
def test_receipt(self):
189189
ins = self.create_install(self.user, self.webapp)
190-
assert ins.receipt.startswith('eyJhbGciOiAiSFMyNTY'), ins.receipt
190+
assert ins.receipt.startswith('eyJhbGciOiAiUlM1MTIiLCA'), ins.receipt
191191

192192
def test_get_receipt(self):
193193
ins = self.create_install(self.user, self.webapp)
@@ -210,3 +210,16 @@ def test_addon_free(self):
210210
self.webapp.update(premium_type=amo.ADDON_FREE)
211211
self.create_install(self.user, self.webapp)
212212
assert self.webapp.get_receipt(self.user)
213+
214+
def test_install_has_email(self):
215+
install = self.create_install(self.user, self.webapp)
216+
eq_(install.email, u'regular@mozilla.com')
217+
218+
def test_install_not_premium(self):
219+
install = self.create_install(self.user, self.webapp)
220+
eq_(install.premium_type, amo.ADDON_FREE)
221+
222+
def test_install_premium(self):
223+
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
224+
install = self.create_install(self.user, self.webapp)
225+
eq_(install.premium_type, amo.ADDON_PREMIUM)

apps/webapps/tests/test_verify.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# -*- coding: utf8 -*-
2+
from django.db import connection
3+
4+
from nose.tools import eq_
5+
6+
import amo
7+
import amo.tests
8+
from addons.models import Addon
9+
from services import verify
10+
from webapps.models import Installed
11+
from market.models import AddonPurchase
12+
from users.models import UserProfile
13+
from stats.models import Contribution
14+
15+
import json
16+
import mock
17+
18+
19+
class TestVerify(amo.tests.TestCase):
20+
fixtures = ['base/addon_3615', 'base/users']
21+
22+
def setUp(self):
23+
self.addon = Addon.objects.get(pk=3615)
24+
self.user = UserProfile.objects.get(email='regular@mozilla.com')
25+
self.user_data = {'user': {'value': self.user.email}}
26+
27+
def get_decode(self, addon_id, receipt):
28+
# Ensure that the verify code is using the test database cursor.
29+
v = verify.Verify(addon_id, receipt)
30+
v.cursor = connection.cursor()
31+
return json.loads(v())
32+
33+
@mock.patch.object(verify, 'decode_receipt')
34+
def get(self, addon_id, receipt, decode_receipt):
35+
decode_receipt.return_value = receipt
36+
return self.get_decode(addon_id, '')
37+
38+
def make_install(self):
39+
return Installed.objects.create(addon=self.addon, user=self.user)
40+
41+
def make_purchase(self):
42+
return AddonPurchase.objects.create(addon=self.addon, user=self.user)
43+
44+
def make_contribution(self, type=amo.CONTRIB_PURCHASE):
45+
return Contribution.objects.create(addon=self.addon, user=self.user,
46+
type=type)
47+
48+
def test_invalid_receipt(self):
49+
eq_(self.get_decode(1, 'blah')['status'], 'invalid')
50+
51+
def test_no_user(self):
52+
eq_(self.get(1, {})['status'], 'invalid')
53+
54+
def test_no_addon(self):
55+
eq_(self.get(0, {'user': {'value': 'a@a.com'}})['status'], 'invalid')
56+
57+
def test_user_addon(self):
58+
self.make_install()
59+
res = self.get(3615, self.user_data)
60+
eq_(res['status'], 'ok')
61+
eq_(res['receipt'], self.user_data)
62+
63+
def test_premium_addon_not_purchased(self):
64+
self.addon.update(premium_type=amo.ADDON_PREMIUM)
65+
self.make_install()
66+
res = self.get(3615, self.user_data)
67+
eq_(res['status'], 'invalid')
68+
69+
def test_premium_addon_purchased(self):
70+
self.addon.update(premium_type=amo.ADDON_PREMIUM)
71+
self.make_install()
72+
self.make_purchase()
73+
res = self.get(3615, self.user_data)
74+
eq_(res['status'], 'ok')
75+
76+
def test_premium_addon_contribution(self):
77+
self.addon.update(premium_type=amo.ADDON_PREMIUM)
78+
self.make_install()
79+
# There's no purchase, but the last entry we have is a sale.
80+
self.make_contribution()
81+
res = self.get(3615, self.user_data)
82+
eq_(res['status'], 'ok')
83+
84+
def test_premium_addon_refund(self):
85+
self.addon.update(premium_type=amo.ADDON_PREMIUM)
86+
self.make_install()
87+
for type in [amo.CONTRIB_REFUND, amo.CONTRIB_CHARGEBACK]:
88+
self.make_contribution(type=type)
89+
res = self.get(3615, self.user_data)
90+
eq_(res['status'], 'refunded')
91+
92+
def test_crack_receipt(self):
93+
# Check that we can decode our receipt and get a dictionary back.
94+
self.addon.update(type=amo.ADDON_WEBAPP, manifest_url='http://a.com')
95+
receipt = self.make_install().receipt
96+
result = verify.decode_receipt(receipt)
97+
eq_(result['typ'], u'purchase-receipt')

docs/topics/services.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
.. _services:
2+
3+
==========================
4+
Services
5+
==========================
6+
7+
Services contain a couple of scripts that are run as seperate wsgi instances on
8+
the services. Usually they are hosted on seperate domains. They are stand alone
9+
wsgi scripts. The goal is to avoid a whole pile of Django imports, middleware,
10+
sessions and so on that we really don't need.
11+
12+
To run the scripts you'll want a wsgi server, on prod this is Apache and
13+
mod_wsgi. Locally you can optionally use `gunicorn`_, for example::
14+
15+
pip install gunicorn
16+
17+
Then you can do::
18+
19+
cd services
20+
gunicorn -c wsgi/verify.wsgi -b 127.0.0.1:9000 --debug verify:application
21+
22+
To test::
23+
24+
curl -d "this is a bogus receipt" http://127.0.0.1:9000/verify/123
25+
26+
.. _`Gunicorn`: http://gunicorn.org/

migrations/273-remove-receipt.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE users_install DROP COLUMN receipt;

migrations/274-add-user-id.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ALTER TABLE users_install ADD COLUMN email varchar(255) NOT NULL;
2+
ALTER TABLE users_install ADD COLUMN premium_type integer UNSIGNED;
3+
CREATE INDEX `users_install_email` ON `users_install` (`email`);

0 commit comments

Comments
 (0)