diff --git a/ccvpn/settings.py b/ccvpn/settings.py index 264835e..f401731 100644 --- a/ccvpn/settings.py +++ b/ccvpn/settings.py @@ -334,6 +334,8 @@ OPENVPN_CONFIG_HEADER = """\ # +----------------------------+ """ +RUN_ONLINE_TESTS = False + # Local settings try: from .local_settings import * # noqa diff --git a/lambdainst/tests.py b/lambdainst/tests.py index 4b5f0b2..6e62ab2 100644 --- a/lambdainst/tests.py +++ b/lambdainst/tests.py @@ -3,7 +3,7 @@ from django.test import TestCase from django.utils import timezone from django.core.management import call_command from django.core import mail -from django.utils.six import StringIO +from io import StringIO from constance import config as site_config from constance.test import override_config @@ -175,64 +175,72 @@ class SignupViewTest(TestCase): class AccountViewsTest(TestCase, UserTestMixin): def setUp(self): - User.objects.create_user('test', None, 'testpw') - self.client.login(username='test', password='testpw') + User.objects.create_user('test', None, 'test_pw') + self.client.login(username='test', password='test_pw') def test_account(self): response = self.client.get('/account/') self.assertEqual(response.status_code, 200) - def test_trial_get(self): - response = self.client.get('/account/trial') - self.assertRedirects(response, '/account/') - - def test_trial(self): - p = timedelta(days=1) - with self.settings(RECAPTCHA_API='TEST'): - with override_config(TRIAL_PERIOD_HOURS=24, TRIAL_PERIOD_MAX=2): - good_data = {'g-recaptcha-response': 'TEST-TOKEN'} - - response = self.client.post('/account/trial', good_data) - self.assertRedirects(response, '/account/') - - user = User.objects.get(username='test') - self.assertRemaining(user.vpnuser, p) - - def test_trial_fail(self): - p = timedelta(days=1) - with self.settings(RECAPTCHA_API='TEST'): - with override_config(TRIAL_PERIOD_HOURS=24, TRIAL_PERIOD_MAX=2): - bad_data = {'g-recaptcha-response': 'TOTALLY-NOT-TEST-TOKEN'} - - response = self.client.post('/account/trial', bad_data) - self.assertRedirects(response, '/account/') - - user = User.objects.get(username='test') - self.assertRemaining(user.vpnuser, timedelta()) - def test_settings_form(self): response = self.client.get('/account/settings') self.assertEqual(response.status_code, 200) - def test_settings_post(self): + def print_message(self, response): + from django.contrib.messages import get_messages + messages = list(get_messages(response.wsgi_request)) + for m in messages: + print(f"[message: {m.message!r} level={m.level} tags={m.tags!r}]") + + def test_settings_post_email(self): response = self.client.post('/account/settings', { - 'password': 'new_test_pw', 'password2': 'new_test_pw', + 'action': 'email', + 'current_password': 'test_pw', 'email': 'new_email@example.com'}) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 302) user = User.objects.get(username='test') - self.assertTrue(user.check_password('new_test_pw')) self.assertEqual(user.email, 'new_email@example.com') - def test_settings_post_fail(self): + def test_settings_post_email_fail(self): response = self.client.post('/account/settings', { - 'password': 'new_test_pw', 'password2': 'new_test_pw_qsdfg', + 'action': 'email', + 'current_password': 'not_test_pw', 'email': 'new_email@example.com'}) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 302) + + user = User.objects.get(username='test') + self.assertNotEqual(user.email, 'new_email@example.com') + + def test_settings_post_pw(self): + response = self.client.post('/account/settings', { + 'action': 'password', + 'current_password': 'test_pw', + 'password': 'new_test_pw', 'password2': 'new_test_pw'}) + self.assertEqual(response.status_code, 302) + + user = User.objects.get(username='test') + self.assertTrue(user.check_password('new_test_pw')) + + def test_settings_post_pw_fail(self): + response = self.client.post('/account/settings', { + 'action': 'password', + 'current_password': 'oops', + 'password': 'new_test_pw', + 'password2': 'new_test_pw'}) + self.assertEqual(response.status_code, 302) + + response = self.client.post('/account/settings', { + 'action': 'password', + 'current_password': 'test_pw', + 'password': 'new_test_pw2', + 'password2': 'new_test_pw_qsdfg'}) + self.assertEqual(response.status_code, 302) user = User.objects.get(username='test') self.assertFalse(user.check_password('new_test_pw')) - self.assertEqual(user.email, 'new_email@example.com') + self.assertFalse(user.check_password('new_test_pw2')) + self.assertTrue(user.check_password('test_pw')) def test_giftcode_use_single(self): gc = GiftCode.objects.create(time=timedelta(days=42), single_use=True) diff --git a/payments/models.py b/payments/models.py index 36566bf..540c021 100644 --- a/payments/models.py +++ b/payments/models.py @@ -96,7 +96,19 @@ def period_months(p): }[p] -class Payment(models.Model): +class BackendData: + backend_data = None + + def set_data(self, key, value): + """ adds a backend data key to this instance's dict """ + if not self.backend_data: + self.backend_data = {} + if not isinstance(self.backend_data, dict): + raise Exception("self.backend_data is not a dict (%r)" % self.backend_data) + self.backend_data[key] = value + + +class Payment(models.Model, BackendData): """ Just a payment. If subscription is not null, it has been automatically issued. backend_extid is the external transaction ID, backend_data is other @@ -170,7 +182,7 @@ class Payment(models.Model): return payment -class Subscription(models.Model): +class Subscription(models.Model, BackendData): """ Recurring payment subscription. """ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) backend_id = models.CharField(max_length=16, choices=BACKEND_CHOICES) diff --git a/payments/tests/__init__.py b/payments/tests/__init__.py index 3c9fba8..118c9ab 100644 --- a/payments/tests/__init__.py +++ b/payments/tests/__init__.py @@ -2,6 +2,9 @@ from .bitcoin import * from .paypal import * -from .stripe import * from .coingate import * +from django.conf import settings +if settings.RUN_ONLINE_TESTS: + from .online.stripe import * + diff --git a/payments/tests/online/stripe.py b/payments/tests/online/stripe.py new file mode 100644 index 0000000..50c3a7f --- /dev/null +++ b/payments/tests/online/stripe.py @@ -0,0 +1,112 @@ +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from selenium.webdriver.firefox.webdriver import WebDriver + +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support.ui import Select +from selenium.webdriver.support.expected_conditions import presence_of_element_located +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By + +from django.conf import settings +from django.utils import timezone +from datetime import timedelta +from lambdainst.models import User +from payments.models import Payment, Subscription + + +class BaseOnlineTest(StaticLiveServerTestCase): + # using a fixed port because you're supposed to forward a public ip to it + # for the web hooks + # $ ssh relaybox -R 12345:127.0.0.1:8000 + # (set ROOT_URL to the right public url and RUN_ONLINE_TESTS to True) + port = 8000 + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.selenium = WebDriver(firefox_binary="/usr/bin/firefox") + cls.selenium.implicitly_wait(6) + + cls.root_url = settings.ROOT_URL + cls.wait = WebDriverWait(cls.selenium, 6) + + @classmethod + def tearDownClass(cls): + cls.selenium.quit() + super().tearDownClass() + + def _signup(self): + self.selenium.get('%s%s' % (self.root_url, '/account/signup')) + username_input = self.selenium.find_element_by_name("username") + username_input.send_keys('test-user') + password_input = self.selenium.find_element_by_name("password") + password_input.send_keys('test-password') + password_input = self.selenium.find_element_by_name("password2") + password_input.send_keys('test-password') + self.selenium.find_element_by_xpath('//input[@value="Sign up"]').click() + + +class OnlineStripeTests(BaseOnlineTest): + fixtures = [] + + def test_payment(self): + self._signup() + + self.selenium.find_element_by_xpath('//label[@for="tab_onetime"]').click() + self.wait.until(EC.visibility_of(self.selenium.find_element_by_xpath('//label[@for="tab_onetime"]/..//select[@name="method"]'))) + self.selenium.find_element_by_xpath('//label[@for="tab_onetime"]/..//select[@name="time"]/option[@value="3"]').click() + self.selenium.find_element_by_xpath('//label[@for="tab_onetime"]/..//select[@name="method"]/option[@value="stripe"]').click() + self.selenium.find_element_by_xpath('//label[@for="tab_onetime"]/..//input[@value="Buy Now"]').click() + + assert self.selenium.find_element_by_xpath('//span[text()="Test Mode"]') + + self.selenium.find_element_by_xpath('//input[@name="email"]').send_keys("test@ccrypto.org") + self.selenium.find_element_by_xpath('//input[@name="cardNumber"]').send_keys("4242424242424242") + self.selenium.find_element_by_xpath('//input[@name="cardExpiry"]').send_keys("6/66") + self.selenium.find_element_by_xpath('//input[@name="cardCvc"]').send_keys("420") + self.selenium.find_element_by_xpath('//input[@name="billingName"]').send_keys("Test User") + self.selenium.find_element_by_xpath('//button[@type="submit"]').click() + + self.selenium.find_element_by_xpath('//h2[contains(text(),"Confirmed")]') + + user = User.objects.get(username="test-user") + assert user.vpnuser.is_paid + assert user.vpnuser.expiration >= (timezone.now() + timedelta(days=89)) + + def test_subscription(self): + self._signup() + + self.selenium.find_element_by_xpath('//label[@for="tab_subscr"]').click() + self.wait.until(EC.visibility_of(self.selenium.find_element_by_xpath('//label[@for="tab_subscr"]/..//select[@name="method"]'))) + self.selenium.find_element_by_xpath('//label[@for="tab_subscr"]/..//select[@name="time"]/option[@value="12"]').click() + self.selenium.find_element_by_xpath('//label[@for="tab_subscr"]/..//select[@name="method"]/option[@value="stripe"]').click() + self.selenium.find_element_by_xpath('//label[@for="tab_subscr"]/..//input[@value="Subscribe"]').click() + + assert self.selenium.find_element_by_xpath('//span[text()="Test Mode"]') + + self.selenium.find_element_by_xpath('//input[@name="email"]').send_keys("test@ccrypto.org") + self.selenium.find_element_by_xpath('//input[@name="cardNumber"]').send_keys("4242424242424242") + self.selenium.find_element_by_xpath('//input[@name="cardExpiry"]').send_keys("6/66") + self.selenium.find_element_by_xpath('//input[@name="cardCvc"]').send_keys("420") + self.selenium.find_element_by_xpath('//input[@name="billingName"]').send_keys("Test User") + self.selenium.find_element_by_xpath('//button[@type="submit"]').click() + + sub_status = self.selenium.find_element_by_xpath('//td[text()="Subscription"]//following-sibling::td') + assert sub_status.text.startswith('ACTIVE') + + user = User.objects.get(username="test-user") + assert user.vpnuser.is_paid + assert user.vpnuser.expiration >= (timezone.now() + timedelta(days=359)) + + sub_status.find_element_by_xpath('a[text()="cancel"]').click() + self.selenium.find_element_by_xpath('//input[@value="Cancel Subscription"]').click() + + sub_status = self.selenium.find_element_by_xpath('//td[text()="Subscription"]//following-sibling::td') + assert not sub_status.text.startswith('ACTIVE') + + user = User.objects.get(username="test-user") + assert user.vpnuser.is_paid + assert user.vpnuser.expiration >= (timezone.now() + timedelta(days=359)) + assert not user.vpnuser.get_subscription() + diff --git a/payments/tests/stripe.py b/payments/tests/stripe.py deleted file mode 100644 index 773550c..0000000 --- a/payments/tests/stripe.py +++ /dev/null @@ -1,400 +0,0 @@ -from datetime import timedelta - -from django.test import TestCase, RequestFactory -from django.contrib.auth.models import User - -from payments.models import Payment, Subscription -from payments.backends import StripeBackend - - -def MockSession(id): - class MockSession(dict): - def __init__(self, args): - args['id'] = id - super().__init__(args) - - @classmethod - def create(cls, **kwargs): - c = MockSession(kwargs) - cls._INST = c - return c - return MockSession - -class MockWebhook: - @classmethod - def construct_event(cls, payload, sig, key): - assert sig == "hewwo", sig - assert key == "test_wh_key", key - assert payload == b"opak", payload - return cls.evt - - -class StripeBackendTest(TestCase): - def setUp(self): - self.user = User.objects.create_user('test', 'test_user@example.com', None) - - def test_stripe(self): - payment = Payment.objects.create( - user=self.user, - time=timedelta(days=30), - backend_id='stripe', - amount=300 - ) - payment.save() - - sess_id = 'cs_test_UKHmmetjaiperdu0wxGppeerrdduuhSslYaaaaaa0eU6aaaaaaaaaWSX' - - settings = dict( - secret_key='test_secret_key', - public_key='test_public_key', - wh_key='test_wh_key', - currency='EUR', - name='Test Name', - ) - - backend = StripeBackend(settings) - assert backend.backend_enabled - - backend.stripe = type('Stripe', (object, ), { - 'checkout': type('checkout', (object, ), { - 'Session': MockSession(sess_id), - }), - 'Webhook': MockWebhook, - - 'error': type('error', (object, ), { - 'InvalidRequestError': type('', (Exception, ), {}), - 'SignatureVerificationError': type('', (Exception, ), {}), - 'CardError': type('CardError', (Exception, ), {}), - }), - }) - - with self.settings(ROOT_URL='root'): - form_html = backend.new_payment(payment) - - assert 'test_public_key' in form_html - - sess = backend.stripe.checkout.Session._INST - self.assertEqual(sess['success_url'], 'root/payments/view/%d' % payment.id) - self.assertEqual(sess['line_items'][0]['amount'], payment.amount) - - request = RequestFactory().post('', 'opak', content_type='text/plain', HTTP_STRIPE_SIGNATURE="hewwo") - - backend.stripe.Webhook.evt = CHECKOUT_SESSION_COMPLETED__ONE - backend.webhook(request) - - payment = Payment.objects.get(id=payment.id) - self.assertEqual(payment.status, 'confirmed') - self.assertEqual(payment.paid_amount, 300) - self.assertEqual(payment.backend_extid, sess_id) - - def test_stripe_sub(self): - sub = Subscription.objects.create( - user=self.user, - period='3m', - backend_id='stripe', - ) - - settings = dict( - secret_key='test_secret_key', - public_key='test_public_key', - wh_key='test_wh_key', - currency='EUR', - name='Test Name', - ) - - backend = StripeBackend(settings) - assert backend.backend_enabled - - backend.stripe = type('Stripe', (object, ), { - 'checkout': type('checkout', (object, ), { - 'Session': MockSession("in_FjSMAw7ufaEBOG"), - }), - 'Webhook': MockWebhook, - - 'error': type('error', (object, ), { - 'InvalidRequestError': type('', (Exception, ), {}), - 'SignatureVerificationError': type('', (Exception, ), {}), - 'CardError': type('CardError', (Exception, ), {}), - }), - }) - - with self.settings(ROOT_URL='root'): - form_html = backend.new_subscription(sub) - - assert 'test_public_key' in form_html - - sess = backend.stripe.checkout.Session._INST - self.assertEqual(sess['success_url'], 'root/payments/return_subscr/%d' % sub.id) - self.assertEqual(sess['subscription_data']['items'][0]['plan'], 'ccvpn_3m') - - request = RequestFactory().post('', 'opak', content_type='text/plain', HTTP_STRIPE_SIGNATURE="hewwo") - - backend.stripe.Webhook.evt = checkout_session_completed__sub(sub) - backend.webhook(request) - backend.stripe.Webhook.evt = INVOICE_PAYMENT_SUCCEEDED - backend.webhook(request) - - sub = Subscription.objects.get(id=sub.id) - payment = sub.payment_set.all()[0] - self.assertEqual(sub.status, 'active') - self.assertEqual(payment.status, 'confirmed') - self.assertEqual(payment.paid_amount, 900) - self.assertEqual(payment.backend_extid, 'in_FjSMAw7ufaEBOG') - - -CHECKOUT_SESSION_COMPLETED__ONE = { - "id": "evt_FkzwrsBSh0da66", - "object": "event", - "api_version": "2019-08-14", - "created": 1567705834, - "data": { - "object": { - "id": "cs_test_UKHmmetjaiperdu0wxGppeerrdduuhSslYaaaaaa0eU6aaaaaaaaaWSX", - "object": "checkout.session", - "billing_address_collection": None, - "cancel_url": "https://test/payments/cancel/19", - "client_reference_id": None, - "customer": "cus_FgusdomerZ8gdP", - "customer_email": None, - "display_items": [ - { - "amount": 300, - "currency": "eur", - "custom": { - "description": "One month for admin", - "images": None, - "name": "VPN Payment" - }, - "quantity": 1, - "type": "custom" - } - ], - "livemode": False, - "locale": None, - "mode": "payment", - "payment_intent": "pi_0FmmmmmmmmmmmmmmmpyplM53", - "payment_method_types": [ - "card" - ], - "setup_intent": None, - "submit_type": None, - "subscription": None, - "success_url": "https://test/payments/view/19" - } - }, - "livemode": False, - "pending_webhooks": 1, - "request": { - "id": None, - "idempotency_key": None - }, - "type": "checkout.session.completed" -} - -def checkout_session_completed__sub(sub): - return { - "id": "evt_FjSIjKT3MvONr8", - "object": "event", - "api_version": "2019-08-14", - "created": 1567368634, - "data": { - "object": { - "id": "cs_test_03aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "object": "checkout.session", - "billing_address_collection": None, - "cancel_url": "https://test/payments/view/2", - "client_reference_id": "sub_%d" % sub.id, - "customer": "cus_FkSIg5BNIdrv6v", - "customer_email": None, - "display_items": [ - { - "amount": 900, - "currency": "eur", - "plan": { - "id": "ccvpn_3m", - "object": "plan", - "active": True, - "aggregate_usage": None, - "amount": 900, - "amount_decimal": "900", - "billing_scheme": "per_unit", - "created": 1557830755, - "currency": "eur", - "interval": "month", - "interval_count": 3, - "livemode": False, - "metadata": { - }, - "nickname": None, - "product": "prod_F062eB2kCukpUX", - "tiers": None, - "tiers_mode": None, - "transform_usage": None, - "trial_period_days": None, - "usage_type": "licensed" - }, - "quantity": 1, - "type": "plan" - } - ], - "livemode": False, - "locale": None, - "mode": "subscription", - "payment_intent": None, - "payment_method_types": [ - "card" - ], - "setup_intent": None, - "submit_type": None, - "subscription": "sub_Fjmeowmeowmeow", - "success_url": "https://test/payments/return/2" - } - }, - "livemode": False, - "pending_webhooks": 1, - "request": { - "id": "req_xQY51312421312", - "idempotency_key": None - }, - "type": "checkout.session.completed" -} - -INVOICE_PAYMENT_SUCCEEDED = { - "id": "evt_FjSMpT5zIn8G9z", - "object": "event", - "api_version": "2019-08-14", - "created": 1567368905, - "data": { - "object": { - "id": "in_FjSMAw7ufaEBOG", - "object": "invoice", - "account_country": "FR", - "account_name": "Cognitive Cryptography", - "amount_due": 900, - "amount_paid": 900, - "amount_remaining": 0, - "application_fee_amount": None, - "attempt_count": 1, - "attempted": True, - "auto_advance": False, - "billing": "charge_automatically", - "billing_reason": "subscription_create", - "charge": "ch_FjSMFp6fbsc45t", - "collection_method": "charge_automatically", - "created": 1567368904, - "currency": "eur", - "custom_fields": None, - "customer": "cus_FjSMgbAnXmL4Go", - "customer_address": None, - "customer_email": "test2@test.test", - "customer_name": None, - "customer_phone": None, - "customer_shipping": None, - "customer_tax_exempt": "none", - "customer_tax_ids": [ - ], - "default_payment_method": None, - "default_source": None, - "default_tax_rates": [ - ], - "description": None, - "discount": None, - "due_date": None, - "ending_balance": 0, - "footer": None, - "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_wmhbGo7OofOofOofOofOofOofd", - "invoice_pdf": "https://pay.stripe.com/invoice/invst_wmhbGo7OofOofOofOofOofOofd/pdf", - "lines": { - "object": "list", - "data": [ - { - "id": "sli_c20baf5dcf8472", - "object": "line_item", - "amount": 900, - "currency": "eur", - "description": "1 × VPN Subscription (3m) (at €9.00 / every 3 months)", - "discountable": True, - "livemode": False, - "metadata": { - }, - "period": { - "end": 1575231304, - "start": 1567368904 - }, - "plan": { - "id": "ccvpn_3m", - "object": "plan", - "active": True, - "aggregate_usage": None, - "amount": 900, - "amount_decimal": "900", - "billing_scheme": "per_unit", - "created": 1557830755, - "currency": "eur", - "interval": "month", - "interval_count": 3, - "livemode": False, - "metadata": { - }, - "nickname": None, - "product": "prod_F46LeBpkCuRpUX", - "tiers": None, - "tiers_mode": None, - "transform_usage": None, - "trial_period_days": None, - "usage_type": "licensed" - }, - "proration": False, - "quantity": 1, - "subscription": "sub_Fjmeowmeowmeow", - "subscription_item": "si_FjSMgsAsZURoWf", - "tax_amounts": [ - ], - "tax_rates": [ - ], - "type": "subscription" - } - ], - "has_more": False, - "total_count": 1, - "url": "/v1/invoices/in_FjSMAw7ufaEBOG/lines" - }, - "livemode": False, - "metadata": { - }, - "next_payment_attempt": None, - "number": "AD4E509A-0001", - "paid": True, - "payment_intent": "pi_0FDzS0UmBGT3Of2k4ol85K2G", - "period_end": 1567368904, - "period_start": 1567368904, - "post_payment_credit_notes_amount": 0, - "pre_payment_credit_notes_amount": 0, - "receipt_number": None, - "starting_balance": 0, - "statement_descriptor": None, - "status": "paid", - "status_transitions": { - "finalized_at": 1567368904, - "marked_uncollectible_at": None, - "paid_at": 1567368905, - "voided_at": None - }, - "subscription": "sub_Fjmeowmeowmeow", - "subtotal": 900, - "tax": None, - "tax_percent": None, - "total": 900, - "total_tax_amounts": [ - ], - "webhooks_delivered_at": None - } - }, - "livemode": False, - "pending_webhooks": 1, - "request": { - "id": "req_6O128256512102", - "idempotency_key": None - }, - "type": "invoice.payment_succeeded" -}