Commit 06597560 by Michael Frey Committed by Vedran Karačić

SDN check support

parent e763ac28
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0023_siteconfiguration_send_refund_notifications'),
]
operations = [
migrations.AddField(
model_name='siteconfiguration',
name='enable_sdn_check',
field=models.BooleanField(default=False, help_text='Enable SDN check at checkout.', verbose_name='Enable SDN check'),
),
migrations.AddField(
model_name='siteconfiguration',
name='sdn_api_key',
field=models.CharField(max_length=255, verbose_name='US Treasury SDN API key', blank=True),
),
migrations.AddField(
model_name='siteconfiguration',
name='sdn_api_list',
field=models.CharField(help_text='A comma seperated list of Treasury OFAC lists to check against.', max_length=255, verbose_name='SDN lists', blank=True),
),
migrations.AddField(
model_name='siteconfiguration',
name='sdn_api_url',
field=models.CharField(max_length=255, verbose_name='US Treasury SDN API URL', blank=True),
),
]
......@@ -131,6 +131,27 @@ class SiteConfiguration(models.Model):
blank=True,
default=False
)
enable_sdn_check = models.BooleanField(
verbose_name=_('Enable SDN check'),
help_text=_('Enable SDN check at checkout.'),
default=False
)
sdn_api_url = models.CharField(
verbose_name=_('US Treasury SDN API URL'),
max_length=255,
blank=True
)
sdn_api_key = models.CharField(
verbose_name=_('US Treasury SDN API key'),
max_length=255,
blank=True
)
sdn_api_list = models.CharField(
verbose_name=_('SDN lists'),
help_text=_('A comma-seperated list of Treasury OFAC lists to check against.'),
max_length=255,
blank=True
)
class Meta(object):
unique_together = ('site', 'partner')
......@@ -349,6 +370,16 @@ class SiteConfiguration(models.Model):
"""
return EdxRestApiClient(settings.ENTERPRISE_API_URL, jwt=self.access_token)
@cached_property
def user_api_client(self):
"""
Returns the API client to access the user API endpoint on LMS.
Returns:
EdxRestApiClient: The client to access the LMS user API service.
"""
return EdxRestApiClient(self.build_lms_url('/api/user/v1/'), jwt=self.access_token)
class User(AbstractUser):
"""Custom user model for use with OIDC."""
......@@ -425,8 +456,8 @@ class User(AbstractUser):
try:
api = EdxRestApiClient(
request.site.siteconfiguration.build_lms_url('/api/user/v1'),
jwt=request.site.siteconfiguration.access_token,
append_slash=False
append_slash=False,
jwt=request.site.siteconfiguration.access_token
)
response = api.accounts(self.username).get()
return response
......@@ -512,6 +543,26 @@ class User(AbstractUser):
log.exception(msg)
raise VerificationStatusError(msg)
def deactivate_account(self, site_configuration):
"""Deactive the user's account.
Args:
site_configuration (SiteConfiguration): The site configuration
from which the LMS account API endpoint is created.
Returns:
Response from the deactivation API endpoint.
"""
try:
api = site_configuration.user_api_client
return api.accounts(self.username).deactivate().post()
except: # pylint: disable=bare-except
log.exception(
'Failed to deactivate account for user [%s]',
self.username
)
raise
class Client(User):
pass
......
import json
import ddt
import httpretty
import mock
......@@ -175,6 +177,26 @@ class UserTests(CourseCatalogTestMixin, LmsApiMockMixin, TestCase):
with self.assertRaises(VerificationStatusError):
user.is_verified(self.site)
@httpretty.activate
def test_deactivation(self):
"""Verify the deactivation endpoint is called for the user."""
user = self.create_user()
expected_response = {'user_deactivated': True}
self.mock_deactivation_api(self.request, user.username, response=json.dumps(expected_response))
self.assertEqual(user.deactivate_account(self.request.site.siteconfiguration), expected_response)
def test_deactivation_exception_handling(self):
"""Verify an error is logged if an exception happens."""
def callback(*args): # pylint: disable=unused-argument
raise ConnectionError
user = self.create_user()
self.mock_deactivation_api(self.request, user.username, response=callback)
with self.assertRaises(ConnectionError):
with mock.patch('ecommerce.core.models.log.exception') as mock_logger:
user.deactivate_account(self.request.site.siteconfiguration)
self.assertTrue(mock_logger.called)
class BusinessClientTests(TestCase):
def test_str(self):
......
import json
import mock
import ddt
from django.core.urlresolvers import reverse
from rest_framework import status
from ecommerce.core.models import User
from ecommerce.extensions.api.v2.tests.views import JSON_CONTENT_TYPE
from ecommerce.extensions.payment.utils import SDNClient
from ecommerce.tests.testcases import TestCase
@ddt.ddt
class SDNCheckViewSetTests(TestCase):
PATH = reverse('api:v2:sdn:search')
def setUp(self):
super(SDNCheckViewSetTests, self).setUp()
user = self.create_user()
self.client.login(username=user.username, password=self.password)
self.site.siteconfiguration.enable_sdn_check = True
self.site.siteconfiguration.save()
def make_request(self):
"""Make a POST request to the endpoint."""
return self.client.post(
self.PATH,
data=json.dumps({
'name': 'Tester',
'country': 'TE'
}),
content_type=JSON_CONTENT_TYPE
)
def test_authentication_required(self):
"""Verify only authenticated users can access endpoint."""
self.client.logout()
self.assertEqual(self.make_request().status_code, status.HTTP_401_UNAUTHORIZED)
def test_sdn_check_match(self):
"""Verify the endpoint returns the number of hits SDN check made."""
with mock.patch.object(SDNClient, 'search') as sdn_validator_mock:
with mock.patch.object(User, 'deactivate_account') as deactivate_account_mock:
sdn_validator_mock.return_value = {'total': 1}
deactivate_account_mock.return_value = True
response = self.make_request()
self.assertEqual(json.loads(response.content)['hits'], 1)
self.assertTrue(sdn_validator_mock.called)
......@@ -15,6 +15,7 @@ from ecommerce.extensions.api.v2.views import (
providers as provider_views,
publication as publication_views,
refunds as refund_views,
sdn as sdn_views,
siteconfiguration as siteconfiguration_views,
stockrecords as stockrecords_views,
vouchers as voucher_views
......@@ -70,6 +71,10 @@ PROVIDER_URLS = [
url(r'^$', provider_views.ProviderViewSet.as_view(), name='list_providers')
]
SDN_URLS = [
url(r'^search/$', sdn_views.SDNCheckViewSet.as_view(), name='search')
]
urlpatterns = [
url(r'^baskets/', include(BASKET_URLS, namespace='baskets')),
url(r'^checkout/$', include(CHECKOUT_URLS, namespace='checkout')),
......@@ -78,6 +83,7 @@ urlpatterns = [
url(r'^providers/', include(PROVIDER_URLS, namespace='providers')),
url(r'^publication/', include(ATOMIC_PUBLICATION_URLS, namespace='publication')),
url(r'^refunds/', include(REFUND_URLS, namespace='refunds')),
url(r'^sdn/', include(SDN_URLS, namespace='sdn'))
]
router = ExtendedSimpleRouter()
......
"""API endpoint for performing an SDN check on users."""
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from ecommerce.extensions.payment.utils import SDNClient
class SDNCheckViewSet(APIView):
"""Performs an SDN check for a given user."""
permission_classes = (IsAuthenticated,)
def post(self, request):
"""
POST handler for the view. User data is posted to this handler
which performs an SDN check and returns whether the user passed
or failed.
"""
name = request.data['name']
country = request.data['country']
hits = 0
site_configuration = request.site.siteconfiguration
if site_configuration.enable_sdn_check:
sdn_check = SDNClient(
api_url=site_configuration.sdn_api_url,
api_key=site_configuration.sdn_api_key,
sdn_list=site_configuration.sdn_api_list
)
response = sdn_check.search(name, country)
hits = response['total']
if hits > 0:
sdn_check.deactivate_user(
request.user,
request.site.siteconfiguration,
name,
country,
response
)
return Response({'hits': hits})
......@@ -279,6 +279,7 @@ class BasketSummaryView(BasketView):
unicode(course_key)
),
'enable_client_side_checkout': False,
'sdn_check': site_configuration.enable_sdn_check
})
payment_processors = site_configuration.get_payment_processors()
......
......@@ -5,6 +5,8 @@ from oscar.apps.payment.admin import * # noqa pylint: disable=wildcard-import,u
from oscar.core.loading import get_model
from solo.admin import SingletonModelAdmin
from ecommerce.extensions.payment.models import SDNCheckFailure
PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse')
PaypalProcessorConfiguration = get_model('payment', 'PaypalProcessorConfiguration')
......@@ -35,4 +37,17 @@ class PaymentProcessorResponseAdmin(admin.ModelAdmin):
formatted_response.allow_tags = True
@admin.register(SDNCheckFailure)
class SDNCheckFailureAdmin(admin.ModelAdmin):
search_fields = ('username', 'full_name')
list_display = ('username', 'full_name', 'country')
fields = ('username', 'full_name', 'country', 'formatted_response')
readonly_fields = ('username', 'full_name', 'country', 'formatted_response')
def formatted_response(self, obj):
pretty_response = pformat(obj.sdn_check_response)
# Use format_html() to escape user-provided inputs, avoiding an XSS vulnerability.
return format_html('<br><br><pre>{}</pre>', pretty_response)
admin.site.register(PaypalProcessorConfiguration, SingletonModelAdmin)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
import jsonfield.fields
import django_extensions.db.fields
class Migration(migrations.Migration):
dependencies = [
('payment', '0012_auto_20161109_1456'),
]
operations = [
migrations.CreateModel(
name='SDNCheckFailure',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', django_extensions.db.fields.CreationDateTimeField(default=django.utils.timezone.now, verbose_name='created', editable=False, blank=True)),
('modified', django_extensions.db.fields.ModificationDateTimeField(default=django.utils.timezone.now, verbose_name='modified', editable=False, blank=True)),
('full_name', models.CharField(max_length=255)),
('username', models.CharField(max_length=255)),
('country', models.CharField(max_length=2)),
('sdn_check_response', jsonfield.fields.JSONField()),
],
options={
'verbose_name': 'SDN Check Failure',
},
),
]
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from jsonfield import JSONField
from oscar.apps.payment.abstract_models import AbstractSource
from django_extensions.db.models import TimeStampedModel
from solo.models import SingletonModel
from ecommerce.extensions.payment.constants import CARD_TYPE_CHOICES
......@@ -46,5 +49,20 @@ class PaypalProcessorConfiguration(SingletonModel):
verbose_name = "Paypal Processor Configuration"
class SDNCheckFailure(TimeStampedModel):
""" Record of SDN check failure. """
full_name = models.CharField(max_length=255)
username = models.CharField(max_length=255)
country = models.CharField(max_length=2)
sdn_check_response = JSONField()
def __unicode__(self):
return 'SDN check failure [{username}]'.format(
username=self.username
)
class Meta(object):
verbose_name = 'SDN Check Failure'
# noinspection PyUnresolvedReferences
from oscar.apps.payment.models import * # noqa pylint: disable=ungrouped-imports, wildcard-import,unused-wildcard-import,wrong-import-position,wrong-import-order
# -*- coding: utf-8 -*-
from ecommerce.tests.testcases import TestCase
from ecommerce.extensions.payment.models import SDNCheckFailure
class SDNCheckFailureTests(TestCase):
def setUp(self):
self.full_name = 'Keyser Söze'
self.username = 'UnusualSuspect'
self.country = 'US'
self.sdn_check_response = {'description': 'Looks a bit suspicious.'}
def test_unicode(self):
""" Verify the __unicode__ method returns the correct value. """
basket = SDNCheckFailure.objects.create(
full_name=self.full_name,
username=self.username,
country=self.country,
sdn_check_response=self.sdn_check_response
)
expected = 'SDN check failure [{username}]'.format(
username=self.username
)
self.assertEqual(unicode(basket), expected)
from ecommerce.extensions.payment.utils import clean_field_value, middle_truncate
import json
import time
from urllib import urlencode
import mock
import httpretty
from django.conf import settings
from django.test import override_settings
from requests.exceptions import HTTPError, Timeout
from ecommerce.core.models import User
from ecommerce.extensions.payment.models import SDNCheckFailure
from ecommerce.extensions.payment.utils import clean_field_value, middle_truncate, SDNClient
from ecommerce.tests.testcases import TestCase
......@@ -22,3 +34,100 @@ class UtilsTests(TestCase):
""" Verify the passed value is cleaned of specific special characters. """
value = 'Some^text:\'test-value'
self.assertEqual(clean_field_value(value), 'Sometexttest-value')
class SDNCheckTests(TestCase):
""" Tests for the SDN check function. """
def setUp(self):
super(SDNCheckTests, self).setUp()
self.name = 'Dr. Evil'
self.country = 'Evilland'
self.user = self.create_user(full_name=self.name)
self.site_configuration = self.site.siteconfiguration
self.site_configuration.enable_sdn_check = True,
self.site_configuration.sdn_api_url = 'http://sdn-test.fake/'
self.site_configuration.sdn_api_key = 'fake-key'
self.site_configuration.sdn_api_list = 'SDN,TEST'
self.site_configuration.save()
self.sdn_validator = SDNClient(
self.site_configuration.sdn_api_url,
self.site_configuration.sdn_api_key,
self.site_configuration.sdn_api_list
)
def mock_sdn_response(self, response, status_code=200):
""" Mock the SDN check API endpoint response. """
params = urlencode({
'sources': self.site_configuration.sdn_api_list,
'api_key': self.site_configuration.sdn_api_key,
'type': 'individual',
'name': self.name,
'countries': self.country
})
sdn_check_url = '{api_url}?{params}'.format(
api_url=self.site_configuration.sdn_api_url,
params=params
)
httpretty.register_uri(
httpretty.GET,
sdn_check_url,
status=status_code,
body=response,
content_type='application/json'
)
def assert_sdn_check_failure_recorded(self, response):
""" Assert an SDN check failure is logged and has the correct values. """
self.assertEqual(SDNCheckFailure.objects.count(), 1)
sdn_object = SDNCheckFailure.objects.first()
self.assertEqual(sdn_object.full_name, self.name)
self.assertEqual(sdn_object.country, self.country)
self.assertEqual(sdn_object.sdn_check_response, response)
@httpretty.activate
@override_settings(SDN_CHECK_REQUEST_TIMEOUT=0.1)
def test_sdn_check_timeout(self):
"""Verify SDN check logs an exception if the request times out."""
def mock_timeout(_request, _uri, headers):
time.sleep(settings.SDN_CHECK_REQUEST_TIMEOUT + 0.1)
return (200, headers, {'total': 1})
self.mock_sdn_response(mock_timeout, status_code=200)
with self.assertRaises(Timeout):
with mock.patch('ecommerce.extensions.payment.utils.logger.exception') as mock_logger:
self.sdn_validator.search(self.name, self.country)
self.assertTrue(mock_logger.called)
@httpretty.activate
def test_sdn_check_connection_error(self):
""" Verify the check logs an exception in case of a connection error. """
self.mock_sdn_response(json.dumps({'total': 1}), status_code=400)
with self.assertRaises(HTTPError):
with mock.patch('ecommerce.extensions.payment.utils.logger.exception') as mock_logger:
self.sdn_validator.search(self.name, self.country)
self.assertTrue(mock_logger.called)
@httpretty.activate
def test_sdn_check_match(self):
""" Verify the SDN check returns the number of matches and records the match. """
sdn_response = {'total': 1}
self.mock_sdn_response(json.dumps(sdn_response))
response = self.sdn_validator.search(self.name, self.country)
self.assertEqual(response, sdn_response)
def test_deactivate_user(self):
""" Verify an SDN failure is logged. """
response = {'description': 'Bad dude.'}
self.assertEqual(SDNCheckFailure.objects.count(), 0)
with mock.patch.object(User, 'deactivate_account') as deactivate_account:
deactivate_account.return_value = True
self.sdn_validator.deactivate_user(
self.user,
self.site_configuration,
self.name,
self.country,
response)
self.assert_sdn_check_failure_recorded(response)
""" Payment-related URLs """
from django.conf.urls import url
from ecommerce.extensions.payment.views import cybersource, PaymentFailedView
from ecommerce.extensions.payment.views import cybersource, PaymentFailedView, SDNFailure
from ecommerce.extensions.payment.views.paypal import PaypalPaymentExecutionView, PaypalProfileAdminView
urlpatterns = [
......@@ -9,6 +9,7 @@ urlpatterns = [
url(r'^cybersource/redirect/$', cybersource.CybersourceInterstitialView.as_view(), name='cybersource_redirect'),
url(r'^cybersource/submit/$', cybersource.CybersourceSubmitView.as_view(), name='cybersource_submit'),
url(r'^error/$', PaymentFailedView.as_view(), name='payment_error'),
url(r'^sdn/failure/$', SDNFailure.as_view(), name='sdn_failure'),
url(r'^paypal/execute/$', PaypalPaymentExecutionView.as_view(), name='paypal_execute'),
url(r'^paypal/profiles/$', PaypalProfileAdminView.as_view(), name='paypal_profiles'),
]
import json
import logging
import re
from urllib import urlencode
import requests
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from oscar.core.loading import get_model
from ecommerce.extensions.payment.models import SDNCheckFailure
logger = logging.getLogger(__name__)
Basket = get_model('basket', 'Basket')
def middle_truncate(string, chars):
......@@ -53,3 +64,73 @@ def clean_field_value(value):
A cleaned string.
"""
return re.sub(r'[\^:"\']', '', value)
class SDNClient(object):
"""A utility class that handles SDN related operations."""
def __init__(self, api_url, api_key, sdn_list):
self.api_url = api_url
self.api_key = api_key
self.sdn_list = sdn_list
def search(self, name, country):
"""
Searches the OFAC list for an individual with the specified details.
The check returns zero hits if:
* request to the SDN API times out
* SDN API returns a non-200 status code response
* user is not found on the SDN list
Args:
name (str): Individual's full name.
country (str): ISO 3166-1 alpha-2 country code where the individual is from.
Returns:
dict: SDN API response.
"""
params = urlencode({
'sources': self.sdn_list,
'api_key': self.api_key,
'type': 'individual',
'name': name,
'countries': country
})
sdn_check_url = '{api_url}?{params}'.format(
api_url=self.api_url,
params=params
)
try:
response = requests.get(sdn_check_url, timeout=settings.SDN_CHECK_REQUEST_TIMEOUT)
except requests.exceptions.Timeout:
logger.exception('Connection to US Treasury SDN API timed out for [%s].', name)
raise
if response.status_code != 200:
logger.exception(
'Unable to connect to US Treasury SDN API for [%s]. Status code [%d] with message: [%s]',
name, response.status_code, response.content
)
raise requests.exceptions.HTTPError('Unable to connect to SDN API')
return json.loads(response.content)
def deactivate_user(self, user, site_configuration, name, country, search_results):
""" Deactivates a user account.
Args:
user (User): User whose account should be deactivated.
site_configuration (SiteConfiguration): The current site's configuration.
name (str): The user's name.
country (str): ISO 3166-1 alpha-2 country code where the individual is from.
search_results (dict): Results from a call to `search` that will
be recorded as the reason for the deactivation.
"""
SDNCheckFailure.objects.create(
full_name=name,
username=user.username,
country=country,
sdn_check_response=search_results
)
logger.warning('SDN check failed for user [%s]', name)
user.deactivate_account(site_configuration)
......@@ -12,3 +12,8 @@ class PaymentFailedView(TemplateView):
'payment_support_email': self.request.site.siteconfiguration.payment_support_email
})
return context
class SDNFailure(TemplateView):
""" Display an error page when the SDN check fails at checkout. """
template_name = 'checkout/sdn_failure.html'
......@@ -244,6 +244,8 @@ CREDIT_PROVIDER_CACHE_TIMEOUT = 600
VOUCHER_CACHE_TIMEOUT = 10 # Value is in seconds.
SDN_CHECK_REQUEST_TIMEOUT = 5 # Value is in seconds.
# APP CONFIGURATION
DJANGO_APPS = [
'django.contrib.admin',
......
......@@ -47,7 +47,7 @@ define([
onFail = function(){
var message = gettext('Problem occurred during checkout. Please contact support');
$('#messages').empty().append(
_s.sprintf('<div class="error">%s</div>', message)
_s.sprintf('<div class="alert alert-error">%s</div>', message)
);
},
onSuccess = function (data) {
......@@ -172,6 +172,33 @@ define([
return $.inArray(cardType, ['amex', 'discover', 'mastercard', 'visa']) > -1;
},
sdnCheck = function(event) {
var first_name = $('input[name=first_name]').val(),
last_name = $('input[name=last_name]').val(),
country = $('select[name=country]').val();
$.ajax({
url: '/api/v2/sdn/search/',
method: 'POST',
contentType: 'application/json; charset=utf-8',
dataType: 'json',
headers: {
'X-CSRFToken': Cookies.get('ecommerce_csrftoken')
},
data: JSON.stringify({
'name': _s.sprintf('%s %s', first_name, last_name),
'country': country
}),
async: false,
success: function(data) {
if (data.hits > 0) {
event.preventDefault();
Utils.redirect('/payment/sdn/failure/');
}
}
});
},
onReady = function() {
var $paymentButtons = $('.payment-buttons'),
basketId = $paymentButtons.data('basket-id');
......@@ -304,6 +331,9 @@ define([
}
cardInfoValidation(e);
cardHolderInfoValidation(e);
if ($('input[name=sdn-check]').val() === 'enabled') {
sdnCheck(e);
}
});
$paymentButtons.find('.payment-button').click(function (e) {
......@@ -360,6 +390,7 @@ define([
onFail: onFail,
onReady: onReady,
onSuccess: onSuccess,
sdnCheck: sdnCheck,
showVoucherForm: showVoucherForm,
};
}
......
......@@ -70,6 +70,9 @@ require([
field.focus();
}
}
} else {
// Unhandled error types should default to the general payment error page.
window.location.href = '/payment/error/';
}
}
}
......
......@@ -222,25 +222,26 @@ define([
});
describe('clientSideCheckoutValidation', function() {
var cc_expiry_months = {
JAN: '01',
FEB: '02',
MAR: '03',
APR: '04',
MAY: '05',
JUN: '06',
JUL: '07',
AUG: '08',
SEP: '09',
OCT: '10',
NOV: '11',
DEC: '12'
};
var cc_expiry_months = {
JAN: '01',
FEB: '02',
MAR: '03',
APR: '04',
MAY: '05',
JUN: '06',
JUL: '07',
AUG: '08',
SEP: '09',
OCT: '10',
NOV: '11',
DEC: '12'
};
beforeEach(function() {
$(
'<fieldset>' +
'<input type="hidden" name="sdn-check" value="disabled">' +
'<div class="form-item"><div><input name="first_name"></div>' +
'<p class="help-block"></p></div>' +
'<div class="form-item"><div><input name="last_name"></div>' +
......@@ -252,9 +253,11 @@ define([
'<div class="form-item"><div><select name="country">' +
'<option value=""><Choose country></option>' +
'<option value="US">United States</option>' +
'<option value="DS">Death Star</option>' +
'</select></div><p class="help-block"></p></div>' +
'<div class="form-item"><div><select name="state">' +
'<option value=""><Choose state></option>' +
'<option value="NY">New York</option>' +
'</select></div><p class="help-block"></p></div>' +
'</fieldset>' +
'<div><input name="card_number">' +
......@@ -283,6 +286,7 @@ define([
$('input[name=address_line1]').val('Central Perk');
$('input[name=city]').val('New York City');
$('select[name=country]').val('US');
$('select[name=state]').val('NY');
BasketPage.onReady();
});
......@@ -319,6 +323,39 @@ define([
).find('~.help-block span').text()
).toEqual('This field is required');
});
it('should perform the SDN check', function() {
var first_name = 'Darth',
last_name = 'Vader',
country = 'DS',
args,
ajaxData,
event = $.Event('click'),
data = {'hits': 1};
$('input[name=first_name]').val(first_name);
$('input[name=last_name]').val(last_name);
$('select[name=country]').val(country);
$('input[name=sdn-check]').val('enabled');
spyOn(Utils, 'redirect');
spyOn(event, 'preventDefault');
spyOn($, 'ajax').and.callFake(function (options) {
options.success(data);
});
BasketPage.sdnCheck(event);
expect($.ajax).toHaveBeenCalled();
expect(event.preventDefault).toHaveBeenCalled();
expect(Utils.redirect).toHaveBeenCalled();
args = $.ajax.calls.argsFor(0)[0];
ajaxData = JSON.parse(args.data);
expect(args.method).toEqual('POST');
expect(args.url).toEqual('/api/v2/sdn/search/');
expect(args.contentType).toEqual('application/json; charset=utf-8');
expect(ajaxData.name).toEqual('Darth Vader');
expect(ajaxData.country).toEqual(country);
});
});
describe('cardInfoValidation', function() {
......
......@@ -218,6 +218,14 @@ define([
});
return invalidDomain;
},
/**
* Redirects the page to the designated path.
* @param {String} path - The path to which to redirect.
*/
redirect: function(path) {
window.location.href = path;
}
};
}
......
......@@ -135,6 +135,9 @@
<div id="card-holder-information" class="form-input-elements placeholder row">
<fieldset>
<legend aria-label={% trans "Card holder information" %}>{% trans "CARD HOLDER INFORMATION" %}</legend>
{% if sdn_check %}
<input type="hidden" name="sdn-check" value="enabled">
{% endif %}
{% crispy payment_form %}
</fieldset>
</div>
......
{% extends 'edx/base.html' %}
{% load i18n %}
{% load staticfiles %}
{% block title %}SDN Check Failure{% endblock %}
{% block navbar %}
{% include 'edx/partials/_student_navbar.html' %}
{% endblock navbar %}
{% block content %}
<div id="error-message">
<div class="container">
<div class="depth depth-2 message-error-content">
<h3>SDN Check Failure</h3>
<p>{% with "<a class='nav-link' href='mailto:ofac.reconsideration@treasury.gov'>"|safe as ofac_email_link %}
{% blocktrans with end_link="</a>"|safe %}
Unfortunately, your account profile or payment information appears to match one or more records on a U.S. Treasury Department sanctions list. This means we cannot complete your transaction or provide you with services and must suspend your learner account.
If you have questions regarding clearing a match, please contact {{ ofac_email_link }}ofac.reconsideration@treasury.gov{{ end_link }} for information about options for clearing a match. Your account will be suspended until this matter is resolved satisfactorily.
{% endblocktrans %}
{% endwith %}</p>
</div>
</div>
</div>
{% endblock %}
......@@ -29,6 +29,7 @@ class SiteConfigurationFactory(factory.DjangoModelFactory):
site = factory.SubFactory(SiteFactory)
partner = factory.SubFactory(PartnerFactory)
send_refund_notifications = False
enable_sdn_check = False
class StockRecordFactory(OscarStockRecordFactory):
......
......@@ -405,3 +405,11 @@ class LmsApiMockMixin(object):
body=json.dumps(verification_data),
content_type=CONTENT_TYPE
)
def mock_deactivation_api(self, request, username, response):
""" Mock deactivation API endpoint. """
url = '{host}/accounts/{username}/deactivate/'.format(
host=request.site.siteconfiguration.build_lms_url('/api/user/v1'),
username=username
)
httpretty.register_uri(httpretty.POST, url, body=response, content_type=CONTENT_TYPE)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment