Skip to content

Commit

Permalink
Merge pull request #766 from WesternFriend/subscription-view-tests
Browse files Browse the repository at this point in the history
Subscription webhook view tests
  • Loading branch information
brylie committed Jul 9, 2023
2 parents 0ce9132 + 4b3df54 commit c00d4ab
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 20 deletions.
13 changes: 13 additions & 0 deletions accounts/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.contrib.auth import get_user_model

import factory
from factory.django import DjangoModelFactory


class UserFactory(DjangoModelFactory):
class Meta:
model = get_user_model()

email = factory.Faker("email")
is_staff = factory.Faker("pybool")
is_active = factory.Faker("pybool")
45 changes: 45 additions & 0 deletions subscription/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import factory
from factory.django import DjangoModelFactory
from factory.fuzzy import FuzzyInteger, FuzzyDate, FuzzyChoice
from django.utils.timezone import now, timedelta
from subscription.models import (
MagazineFormatChoices,
MagazinePriceGroupChoices,
Subscription,
)

from accounts.factories import UserFactory


class SubscriptionFactory(DjangoModelFactory):
class Meta:
model = Subscription

magazine_format = FuzzyChoice(MagazineFormatChoices.values)
price_group = FuzzyChoice(MagazinePriceGroupChoices.values)
price = FuzzyInteger(0, 100)
recurring = factory.Faker("pybool")
start_date = FuzzyDate(
start_date=now().date() - timedelta(days=365),
end_date=now().date(),
)
end_date = FuzzyDate(
start_date=now().date(),
end_date=now().date() + timedelta(days=365),
)
subscriber_given_name = factory.Faker("first_name")
subscriber_family_name = factory.Faker("last_name")
subscriber_organization = factory.Faker("company")
subscriber_street_address = factory.Faker("street_address")
subscriber_street_address_line_2 = factory.Faker("secondary_address")
subscriber_postal_code = factory.Faker("postcode")
subscriber_address_locality = factory.Faker("city")
subscriber_address_region = factory.Faker("state")
subscriber_address_country = factory.Faker("country")

user = factory.SubFactory(UserFactory)
paid = factory.Faker("pybool")

braintree_subscription_id = factory.Sequence(
lambda n: f"braintree_subscription_id_{n}",
)
32 changes: 15 additions & 17 deletions subscription/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,19 @@

logger = logging.getLogger(__name__)

# TODO: convert to a models.TextChoices class
# e.g. in donations/models.py
MAGAZINE_FORMAT_CHOICES = [
("pdf", "PDF"),
("print", "Print"),
("print_and_pdf", "Print and PDF"),
]
# TODO: convert to a models.TextChoices class
# e.g. in donations/models.py
MAGAZINE_PRICE_GROUP_CHOICES = [
("normal", "Normal"),
("true_cost", "True cost"),
("low_income", "Low income"),
("international", "International"),
]

class MagazineFormatChoices(models.TextChoices):
PDF = "pdf", "PDF"
PRINT = "print", "Print"
PRINT_AND_PDF = "print_and_pdf", "Print and PDF"


class MagazinePriceGroupChoices(models.TextChoices):
NORMAL = "normal", "Normal"
TRUE_COST = "true_cost", "True cost"
LOW_INCOME = "low_income", "Low income"
INTERNATIONAL = "international", "International"


SUBSCRIPTION_PRICE_COMPONENTS = {
"normal": {
Expand Down Expand Up @@ -103,12 +101,12 @@ def process_subscription_form(
class Subscription(models.Model):
magazine_format = models.CharField(
max_length=255,
choices=MAGAZINE_FORMAT_CHOICES,
choices=MagazineFormatChoices.choices,
default="pdf",
)
price_group = models.CharField(
max_length=255,
choices=MAGAZINE_PRICE_GROUP_CHOICES,
choices=MagazinePriceGroupChoices.choices,
default="normal",
)
price = models.IntegerField(
Expand Down
147 changes: 145 additions & 2 deletions subscription/tests.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import datetime
import braintree
from django.test import TestCase
from django.test import TestCase, Client
from django.urls import reverse
import json
from unittest.mock import Mock, patch

from accounts.models import User
from subscription.factories import SubscriptionFactory
from subscription.models import Subscription
from .views import handle_subscription_webhook
from .views import GRACE_PERIOD_DAYS, handle_subscription_webhook


class SubscriptionWebhookTestCase(TestCase):
Expand Down Expand Up @@ -146,3 +150,142 @@ def test_subscription_str(self) -> None:
def tearDown(self) -> None:
self.user.delete()
return super().tearDown()


class SubscriptionWebhookViewTests(TestCase):
def setUp(self) -> None:
self.subscription = SubscriptionFactory(
braintree_subscription_id="test_subscription_id",
)

self.client = Client()
self.url = reverse("braintree-subscription-webhook")

self.webhook_notification = {
"bt_signature": "signature",
"bt_payload": "payload",
}

@patch("subscription.views.braintree_gateway.webhook_notification.parse")
def test_csrf_exempt(
self,
mock_parse: Mock,
) -> None:
# Get the current date and time
now = datetime.datetime.now()
one_year_later = now + datetime.timedelta(days=365)

# Set paid_through_date to one year from now
mock_braintree_subscription = Mock()
mock_braintree_subscription.id = self.subscription.braintree_subscription_id # type: ignore # noqa: E501
mock_braintree_subscription.paid_through_date = one_year_later

mock_webhook_notification = Mock()
mock_webhook_notification.kind = "subscription_charged_successfully"
mock_webhook_notification.subscription = mock_braintree_subscription

# mock parse method to return the mock notification
mock_parse.return_value = mock_webhook_notification

csrf_client = Client(enforce_csrf_checks=True)
response = csrf_client.post(
self.url,
data=json.dumps(self.webhook_notification),
content_type="application/json",
)

# assert that parse was called with the correct arguments
mock_parse.assert_called_once_with(
self.webhook_notification["bt_signature"],
self.webhook_notification["bt_payload"],
)

# check the response status code
self.assertEqual(response.status_code, 200)

@patch("subscription.views.braintree_gateway.webhook_notification.parse")
def test_subscription_end_date_updated_with_paid_through_date(
self,
mock_parse: Mock,
) -> None:
# Get the current date without time
today = datetime.date.today()
one_year_later = today + datetime.timedelta(days=365)

# Set paid_through_date to one year from now
mock_braintree_subscription = Mock()
mock_braintree_subscription.id = self.subscription.braintree_subscription_id # type: ignore # noqa: E501
mock_braintree_subscription.paid_through_date = one_year_later

mock_webhook_notification = Mock()
mock_webhook_notification.kind = "subscription_charged_successfully"
mock_webhook_notification.subscription = mock_braintree_subscription

# mock parse method to return the mock notification
mock_parse.return_value = mock_webhook_notification

csrf_client = Client(enforce_csrf_checks=True)
response = csrf_client.post(
self.url,
data=json.dumps(self.webhook_notification),
content_type="application/json",
)

# assert that parse was called with the correct arguments
mock_parse.assert_called_once_with(
self.webhook_notification["bt_signature"],
self.webhook_notification["bt_payload"],
)

# check the response status code
self.assertEqual(response.status_code, 200)

# Refresh subscription from db
self.subscription.refresh_from_db()

# check that the end_date is updated
expected_end_date = one_year_later + GRACE_PERIOD_DAYS
self.assertEqual(self.subscription.end_date, expected_end_date)

@patch("subscription.views.braintree_gateway.webhook_notification.parse")
def test_subscription_end_date_updated_without_paid_through_date(
self,
mock_parse: Mock,
) -> None:
# Calculate the expected end_date
one_year_with_grace_period = datetime.timedelta(days=365) + GRACE_PERIOD_DAYS
expected_end_date = self.subscription.end_date + one_year_with_grace_period

# Set up the Braintree subscription mock without paid_through_date
mock_braintree_subscription = Mock()
mock_braintree_subscription.id = self.subscription.braintree_subscription_id # type: ignore # noqa: E501
mock_braintree_subscription.paid_through_date = None

mock_webhook_notification = Mock()
mock_webhook_notification.kind = "subscription_charged_successfully"
mock_webhook_notification.subscription = mock_braintree_subscription

# mock parse method to return the mock notification
mock_parse.return_value = mock_webhook_notification

csrf_client = Client(enforce_csrf_checks=True)
response = csrf_client.post(
self.url,
data=json.dumps(self.webhook_notification),
content_type="application/json",
)

# assert that parse was called with the correct arguments
mock_parse.assert_called_once_with(
self.webhook_notification["bt_signature"],
self.webhook_notification["bt_payload"],
)

# check the response status code
self.assertEqual(response.status_code, 200)

# Refresh subscription from db
self.subscription.refresh_from_db()

# check that the end_date is updated
self.assertEqual(self.subscription.end_date, expected_end_date)
6 changes: 5 additions & 1 deletion subscription/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
from subscription.views import SubscriptionWebhookView

urlpatterns = [
path("braintree-subscription-webhook/", SubscriptionWebhookView.as_view()),
path(
"braintree-subscription-webhook/",
SubscriptionWebhookView.as_view(),
name="braintree-subscription-webhook",
),
]

0 comments on commit c00d4ab

Please sign in to comment.