Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

feat: create ios products on appstore for given course key #4090

Merged
merged 5 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ecommerce/extensions/iap/api/v1/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
ERROR_DURING_POST_ORDER_OP = "An error occurred during post order operations."
FOUND_MULTIPLE_PRODUCTS_ERROR = "Found unexpected number of products for course [%s]"
GOOGLE_PUBLISHER_API_SCOPE = "https://www.googleapis.com/auth/androidpublisher"
IOS_PRODUCT_REVIEW_NOTE = ('This in-app purchase will unlock all the content of the course {course_name}\n\n'
'For testing the end-to-end payment flow, please follow the following steps:\n1. '
'Go to the Discover tab\n2. Search for "{course_name}"\n3. Enroll in the course'
' "{course_name}"\n4. Hit \"Upgrade to access more features\", it will open a '
'detail unlock features page\n5. Hit "Upgrade now for ${course_price}" from the'
' detail page')
IGNORE_NON_REFUND_NOTIFICATION_FROM_APPLE = "Ignoring notification from apple since we are only expecting" \
" refund notifications"
LOGGER_BASKET_ALREADY_PURCHASED = "Basket creation failed for user [%s] with SKUS [%s]. Products already purchased"
Expand Down
171 changes: 170 additions & 1 deletion ecommerce/extensions/iap/api/v1/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import mock
from django.conf import settings

from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.extensions.iap.api.v1.utils import products_in_basket_already_purchased
from ecommerce.extensions.iap.api.v1.utils import (
AppStoreRequestException,
apply_price_of_inapp_purchase,
create_inapp_purchase,
get_auth_headers,
localize_inapp_purchase,
products_in_basket_already_purchased,
submit_in_app_purchase_for_review,
upload_screenshot_of_inapp_purchase
)
from ecommerce.extensions.order.utils import UserAlreadyPlacedOrder
from ecommerce.extensions.test.factories import create_basket, create_order
from ecommerce.tests.testcases import TestCase
Expand Down Expand Up @@ -37,3 +47,162 @@ def test_not_purchased_yet(self):
with mock.patch.object(UserAlreadyPlacedOrder, 'user_already_placed_order', return_value=False):
return_value = products_in_basket_already_purchased(self.user, self.basket, self.site)
self.assertFalse(return_value)


@mock.patch('ecommerce.extensions.iap.api.v1.utils.jwt.encode', return_value='Test token')
class TestCreateIosProducts(TestCase):
""" Tests for ios product creation on appstore. """

def setUp(self):
super(TestCreateIosProducts, self).setUp()
self.configuration = settings.PAYMENT_PROCESSOR_CONFIG['edx']['ios-iap']

def test_get_auth_headers(self, _):
"""
Test auth headers are returned in required format
"""
headers = {
"Authorization": "Bearer Test token",
"Content-Type": "application/json"
}
self.assertEqual(headers, get_auth_headers(self.configuration))

def test_create_inapp_purchase(self, _):
"""
Test create in app product call and its exception working properly.
"""

with mock.patch('ecommerce.extensions.iap.api.v1.utils.requests.Session.post') as post_call:
post_call.return_value.status_code = 201
course = {
'key': 'test',
'name': 'test',
'price': '123'
}
headers = get_auth_headers(self.configuration)
create_inapp_purchase(course, 'test.sku', '123', headers)
create_url = 'https://api.appstoreconnect.apple.com/v2/inAppPurchases'
self.assertEqual(post_call.call_args[0][0], create_url)
self.assertEqual(post_call.call_args[1]['headers'], headers)
with self.assertRaises(AppStoreRequestException, msg="Couldn't create inapp purchase id"):
post_call.return_value.status_code = 500
create_inapp_purchase(course, 'test.sku', '123', headers)

def test_localize_inapp_purchase(self, _):
"""
Test localize in app product call and its exception working properly.
"""
with mock.patch('ecommerce.extensions.iap.api.v1.utils.requests.Session.post') as post_call:
post_call.return_value.status_code = 201
headers = get_auth_headers(self.configuration)
localize_inapp_purchase('123', headers)
localize_url = 'https://api.appstoreconnect.apple.com/v1/inAppPurchaseLocalizations'
self.assertEqual(post_call.call_args[0][0], localize_url)
self.assertEqual(post_call.call_args[1]['headers'], headers)

with self.assertRaises(AppStoreRequestException, msg="Couldn't localize purchase"):
post_call.return_value.status_code = 500
localize_inapp_purchase('123', headers)

def test_apply_price_of_inapp_purchase(self, _):
"""
Test applying price on in app product call and its exception working properly.
"""
headers = get_auth_headers(self.configuration)
with mock.patch('ecommerce.extensions.iap.api.v1.utils.requests.Session.post') as post_call, \
mock.patch('ecommerce.extensions.iap.api.v1.utils.requests.Session.get') as get_call:
with self.assertRaises(AppStoreRequestException, msg="Couldn't fetch price points"):
get_call.return_value.status_code = 500
apply_price_of_inapp_purchase(100, '123', headers)

get_call.return_value.status_code = 200
get_call.return_value.json.return_value = {
'data': [
{
'id': '1234',
'attributes': {
'customerPrice': '99'
}
}
]
}
with self.assertRaises(AppStoreRequestException, msg="Couldn't find nearest low price point"):
# Make sure it doesn't select higher price point
apply_price_of_inapp_purchase(80, '123', headers)

post_call.return_value.status_code = 201
apply_price_of_inapp_purchase(100, '123', headers)
price_url = 'https://api.appstoreconnect.apple.com/v1/inAppPurchasePriceSchedules'
self.assertEqual(post_call.call_args[0][0], price_url)
self.assertEqual(post_call.call_args[1]['headers'], headers)

with self.assertRaises(AppStoreRequestException, msg="Couldn't apply price"):
post_call.return_value.status_code = 500
apply_price_of_inapp_purchase(100, '123', headers)

def test_upload_screenshot_of_inapp_purchase(self, _):
"""
Test image uploading call for app product and its exception working properly.
"""
headers = get_auth_headers(self.configuration)
with mock.patch('ecommerce.extensions.iap.api.v1.utils.requests.Session.post') as post_call, \
mock.patch('ecommerce.extensions.iap.api.v1.utils.requests.Session.put') as put_call, \
mock.patch('ecommerce.extensions.iap.api.v1.utils.requests.Session.patch') as patch_call, \
mock.patch('django.contrib.staticfiles.storage.staticfiles_storage.open'):

with self.assertRaises(AppStoreRequestException, msg="Couldn't get screenshot url"):
post_call.return_value.status_code = 500
upload_screenshot_of_inapp_purchase('100', headers)

post_call.return_value.status_code = 201
post_call.return_value.json.return_value = {
'data': {
'id': '1234',
'attributes': {
'uploadOperations': [
{'url': 'https://image-url.com'}
]
}
}
}

with self.assertRaises(AppStoreRequestException, msg="Couldn't upload screenshot"):
# Make sure it doesn't select higher price point
upload_screenshot_of_inapp_purchase('123', headers)

put_call.return_value.status_code = 200

with self.assertRaises(AppStoreRequestException, msg="Couldn't finalize screenshot"):
# Make sure it doesn't select higher price point
upload_screenshot_of_inapp_purchase('123', headers)

patch_call.return_value.status_code = 200

upload_screenshot_of_inapp_purchase('123', headers)
img_url = 'https://api.appstoreconnect.apple.com/v1/inAppPurchaseAppStoreReviewScreenshots'
self.assertEqual(post_call.call_args[0][0], img_url)
self.assertEqual(post_call.call_args[1]['headers'], headers)

self.assertEqual(put_call.call_args[0][0], 'https://image-url.com')
img_headers = headers.copy()
img_headers['Content-Type'] = 'image/png'
self.assertEqual(put_call.call_args[1]['headers'], img_headers)

img_patch_url = 'https://api.appstoreconnect.apple.com/v1/inAppPurchaseAppStoreReviewScreenshots/1234'
self.assertEqual(patch_call.call_args[0][0], img_patch_url)
self.assertEqual(patch_call.call_args[1]['headers'], headers)

def submit_in_app_purchase_for_review(self, _):
"""
Test submitting in app product call and its exception working properly.
"""
headers = get_auth_headers(self.configuration)
with mock.patch('ecommerce.extensions.iap.api.v1.utils.requests.Session.post') as post_call:
with self.assertRaises(AppStoreRequestException, msg="Couldn't submit purchase"):
post_call.return_value.status_code = 500
submit_in_app_purchase_for_review('100', headers)

post_call.return_value.status_code = 201
submit_url = 'https://api.appstoreconnect.apple.com/v1/inAppPurchaseSubmissions'
self.assertEqual(post_call.call_args[0][0], submit_url)
self.assertEqual(post_call.call_args[1]['headers'], headers)
22 changes: 13 additions & 9 deletions ecommerce/extensions/iap/api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1074,14 +1074,17 @@ def test_empty_list(self):
self.assertEqual(response.status_code, 200)
result = json.loads(response.content)
expected_result = {
"new_mobile_skus": {},
"failed_course_ids": [],
"missing_course_runs": []
'new_mobile_skus': {},
'failed_course_ids': [],
'missing_course_runs': [],
'failed_ios_products': []
}
self.assertEqual(result, expected_result)

def test_missing_and_new_skus_in_course(self):
@mock.patch('ecommerce.extensions.iap.api.v1.views.create_ios_product')
def test_missing_and_new_skus_in_course(self, create_ios_product_patch):
""" Verify the view differentiate between a correct and non-existent course id """
create_ios_product_patch.return_value = None
with LogCapture(self.logger_name) as logger:
post_data = {"courses": ["course:wrong-id", self.course.id]}
response = self.client.post(self.path, data=post_data, content_type="application/json")
Expand All @@ -1092,15 +1095,16 @@ def test_missing_and_new_skus_in_course(self):
stock_record = StockRecord.objects.get(product=self.product)

expected_result = {
"new_mobile_skus": {
'new_mobile_skus': {
self.course.id:
[
"mobile.android.{}".format(stock_record.partner_sku.lower()),
"mobile.ios.{}".format(stock_record.partner_sku.lower()),
'mobile.android.{}'.format(stock_record.partner_sku.lower()),
'mobile.ios.{}'.format(stock_record.partner_sku.lower()),
]
},
"failed_course_ids": [],
"missing_course_runs": ['course:wrong-id']
'failed_course_ids': [],
'missing_course_runs': ['course:wrong-id'],
'failed_ios_products': []
}

self.assertEqual(result, expected_result)
Loading
Loading