Add fake datatrans endpoints for e2e tests

This commit is contained in:
Daniel Egger 2024-05-31 14:05:42 +02:00
parent ec21238ece
commit f8c6e135e1
16 changed files with 285 additions and 104 deletions

View File

@ -211,7 +211,6 @@ const executePayment = async () => {
</p>
<h3 class="mb-4 mt-10">{{ $t("a.Adresse") }}</h3>
<pre>{{ address }}</pre>
<p class="mb-2">
{{
$t(

View File

@ -675,8 +675,12 @@ if APP_ENVIRONMENT.startswith("prod"):
DATATRANS_API_ENDPOINT = "https://api.datatrans.com"
DATATRANS_PAY_URL = "https://pay.datatrans.com"
else:
DATATRANS_API_ENDPOINT = "https://api.sandbox.datatrans.com"
DATATRANS_PAY_URL = "https://pay.sandbox.datatrans.com"
DATATRANS_API_ENDPOINT = env(
"DATATRANS_API_ENDPOINT", default="https://api.sandbox.datatrans.com"
)
DATATRANS_PAY_URL = env(
"DATATRANS_PAY_URL", default="https://pay.sandbox.datatrans.com"
)
# Only for debugging the webhook (locally)
DATATRANS_DEBUG_WEBHOOK_OVERWRITE = env(

View File

@ -14,6 +14,9 @@ from .base import * # noqa
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
DATABASES["default"]["NAME"] = "vbv_lernwelt_cypress"
DATATRANS_API_ENDPOINT = 'http://localhost:8001/server/fakeapi/datatrans/api'
DATATRANS_PAY_URL = 'http://localhost:8001/server/fakeapi/datatrans/pay'
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend

View File

@ -68,6 +68,11 @@ from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as media_library_urls
from vbv_lernwelt.shop.datatrans_fake_server import (
fake_datatrans_api_view,
fake_datatrans_pay_view,
)
class SignedIntConverter(IntConverter):
regex = r"-?\d+"
@ -242,6 +247,16 @@ if settings.DEBUG:
# Static file serving when using Gunicorn + Uvicorn for local web socket development
urlpatterns += staticfiles_urlpatterns()
if "fakeapi" in settings.DATATRANS_API_ENDPOINT:
urlpatterns += [
re_path(
r"^server/fakeapi/datatrans/api(?P<api_url>.*)$", fake_datatrans_api_view
),
re_path(
r"^server/fakeapi/datatrans/pay(?P<api_url>.*)$", fake_datatrans_pay_view
),
]
# fmt: on

View File

@ -13,7 +13,7 @@ class MeUserViewTest(APITestCase):
)
self.client.login(username="testuser", password="testpassword")
add_organisations()
add_countries()
add_countries(small_set=True)
def test_user_can_update_language(self) -> None:
# GIVEN

View File

@ -1011,7 +1011,7 @@ countries = {
}
def add_countries(apps=None, schema_editor=None):
def add_countries(apps=None, schema_editor=None, small_set=False):
if apps is None:
# pylint: disable=import-outside-toplevel
from vbv_lernwelt.core.models import Country
@ -1025,6 +1025,9 @@ def add_countries(apps=None, schema_editor=None):
for country_id, country_name in countries.items():
country_code = country_name["country_code"]
if small_set and country_code not in ["CH", "LI", "DE", "AT", "FR", "IT"]:
continue
if has_country_code:
Country.objects.get_or_create(
country_code=country_code,

View File

@ -76,4 +76,6 @@ After everything runs fine, we should be able to remove the following deprecated
### Datatrans Test Credit Card
5100 0010 0000 0014 06/25 123
5100 0010 0000 0014
06/25
123

View File

@ -0,0 +1,161 @@
import hashlib
import hmac
import json
import threading
from django.conf import settings
from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect
from django.views.decorators.csrf import csrf_exempt
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
from vbv_lernwelt.core.models import User
@csrf_exempt
@django_view_authentication_exempt
def fake_datatrans_api_view(request, api_url=""):
# if api_url == "/redirect":
# fake_tamedia_token = request.GET.get("token")
# pai = fake_tamedia_token.split(":")[1]
# sub = SubhubCustomerSubscription.objects.filter(id=pai).first()
#
# header = f"<h1>fake tamedia activation for {pai}"
#
# if not sub:
# return HttpResponse(
# content=f"""
# {header}
# <p>no subscription found</p>
# """,
# status=404,
# )
#
# if request.method == "GET":
# if (
# sub
# and sub.partner_status
# == SubhubCustomerSubscription.PARTNER_STATUS_ENROLLED
# ):
# return HttpResponse(
# content=f"""
# {header}
# <div>
# <input type="text" name="partnerSubscriptionId" value="{pai}">
# <form action="{create_register_url(fake_tamedia_token)}" method="post">
# <button type="submit" data-cy="register-button">Register</button>
# </form>
# </div>
# """,
# status=200,
# )
# else:
# return HttpResponse(
# content=f"""
# {header}
# <p>already activated</p>
# """,
# status=200,
# )
# if request.method == "POST":
# if sub:
# response = requests.post(
# f"{settings.APPLICATION_ABSOLUTE_URL}/subhub/ottwebhook",
# json={
# "PartnerIntegration": {
# "effectiveDate": datetime.now().isoformat(),
# "eventType": "activation",
# "offerId": sub.subscription_choice.partner_product_id,
# "optionalAttributes": None,
# "pai": pai,
# "partnerType": "Tamedia",
# "transactionId": str(uuid.uuid4()),
# },
# "eventId": str(uuid.uuid4()),
# "eventType": "OTT Partner Events",
# "publisherId": "Partner Events",
# "status": "new",
# "timestamp": datetime.now().isoformat(),
# },
# auth=HTTPBasicAuth(
# "swisscom_ott_webhook",
# "swisscom-ott-webhook-rLaYG0btVJMPtfnzfLilZtm50",
# ),
# )
# print(response)
# return redirect(f"{create_register_url(fake_tamedia_token)}")
#
# if api_url.startswith("/enroll") and request.method == "POST":
# return HttpResponse(status=204)
#
if api_url == "/v1/transactions" and request.method == "POST":
data = json.loads(request.body.decode("utf-8"))
user = User.objects.get(id=data["user_id"])
user.additional_json_data["datatrans_transaction_payload"] = data
user.save()
return JsonResponse({"transactionId": data["refno"]}, status=201)
return HttpResponse(
content="unknown api url", content_type="application/json", status=400
)
@csrf_exempt
@django_view_authentication_exempt
def fake_datatrans_pay_view(request, api_url=""):
def call_transaction_complete_webhook(
webhook_url, transaction_id, datatrans_status="settled"
):
import requests
import time
time.sleep(1)
payload = {
"transactionId": transaction_id,
"status": datatrans_status,
}
key_hex_bytes = bytes.fromhex(settings.DATATRANS_HMAC_KEY)
# Create sign with timestamp and payload
sign = hmac.new(key_hex_bytes, bytes(str(1) + json.dumps(payload), "utf-8"), hashlib.sha256)
response = requests.post(
url=webhook_url,
json=payload,
headers={
"Datatrans-Signature": f"t=1,s0={sign.hexdigest()}"
},
)
print(response)
if api_url.startswith("/v1/start/") and request.method == "GET":
transaction_id = api_url.split("/")[-1]
transaction_user = User.objects.filter(
additional_json_data__datatrans_transaction_payload__refno=transaction_id
).first()
redirect_url = transaction_user.additional_json_data[
"datatrans_transaction_payload"
]["redirect"]["successUrl"]
# start new thread which will call webhook after 2 seconds
webhook_url = transaction_user.additional_json_data[
"datatrans_transaction_payload"
]["webhook"]["url"]
thread = threading.Thread(
target=call_transaction_complete_webhook,
args=(
webhook_url,
transaction_id,
),
)
thread.start()
# redirect to url
return redirect(redirect_url + f"?datatransTrxId={transaction_id}")
return HttpResponse(
content="unknown api url", content_type="application/json", status=400
)

View File

@ -72,17 +72,34 @@ def create_customer_xml(checkout_information: CheckoutInformation):
abacus_debitor_number=customer.abacus_debitor_number,
last_name=checkout_information.last_name,
first_name=checkout_information.first_name,
company_name=checkout_information.company_name,
street=(checkout_information.company_street or checkout_information.street),
company_name=checkout_information.organisation_detail_name
if checkout_information.invoice_address == "org"
else "",
street=(
checkout_information.organisation_street
if checkout_information.invoice_address == "org"
else checkout_information.street
),
house_number=(
checkout_information.company_street_number
or checkout_information.street_number
checkout_information.organisation_street_number
if checkout_information.invoice_address == "org"
else checkout_information.street_number
),
zip_code=(
checkout_information.company_postal_code or checkout_information.postal_code
checkout_information.organisation_postal_code
if checkout_information.invoice_address == "org"
else checkout_information.postal_code
),
city=(
checkout_information.organisation_city
if checkout_information.invoice_address == "org"
else checkout_information.city
),
country=(
checkout_information.organisation_country_id
if checkout_information.invoice_address == "org"
else checkout_information.country_id
),
city=(checkout_information.company_city or checkout_information.city),
country=(checkout_information.company_country or checkout_information.country),
language=customer.language,
email=customer.email,
)
@ -187,7 +204,8 @@ def render_customer_xml(
SubElement(address_data, "AddressNumber").text = str(abacus_debitor_number)
SubElement(address_data, "Name").text = last_name
SubElement(address_data, "FirstName").text = first_name
SubElement(address_data, "Text").text = company_name
if company_name:
SubElement(address_data, "Text").text = company_name
SubElement(address_data, "Street").text = street
SubElement(address_data, "HouseNumber").text = house_number
SubElement(address_data, "ZIP").text = zip_code

View File

@ -13,7 +13,8 @@ def migrate_checkout_information_country(apps, schema_editor):
if info.old_company_country:
country = Country.objects.get(vbv_country_id=info.old_company_country)
info.organisation_country = country
info.save(update_fields=["country", "organisation_country"])
info.invoice_address = "org"
info.save(update_fields=["country", "organisation_country", "invoice_address"])
class Migration(migrations.Migration):
@ -103,4 +104,7 @@ class Migration(migrations.Migration):
model_name="checkoutinformation",
name="old_company_country",
),
migrations.DeleteModel(
name='BillingAddress',
),
]

View File

@ -45,7 +45,7 @@ def is_signature_valid(
return s0_actual == s0_expected
def init_transaction(
def init_datatrans_transaction(
user: User,
amount_chf_centimes: int,
redirect_url_success: str,
@ -76,6 +76,10 @@ def init_transaction(
},
}
# add testing configuration data
if 'fakeapi' in settings.DATATRANS_API_ENDPOINT:
payload['user_id'] = str(user.id)
logger.info("Initiating transaction", payload=payload)
response = requests.post(

View File

@ -21,13 +21,14 @@ TEST_ADDRESS_DATA = {
"street_number": "1",
"postal_code": "1234",
"city": "Test City",
"country": "209",
"company_name": "Test Company",
"company_street": "Test Company Street",
"company_street_number": "1",
"company_postal_code": "1234",
"company_city": "Test Company City",
"company_country": "209",
"country_code": "CH",
"invoice_address": "org",
"organisation_detail_name": "Test Company",
"organisation_street": "Test Company Street",
"organisation_street_number": "1",
"organisation_postal_code": "1234",
"organisation_city": "Test Company City",
"organisation_country_code": "CH",
}
REDIRECT_URL = "http://testserver/redirect-url"
@ -50,40 +51,9 @@ class CheckoutAPITestCase(APITestCase):
)
self.client.login(username=USER_USERNAME, password=USER_PASSWORD)
add_countries()
add_countries(small_set=True)
@patch("vbv_lernwelt.shop.views.init_transaction")
def test_checkout_no_company_address_updates_user(self, mock_init_transaction):
# GIVEN
mock_init_transaction.return_value = "1234567890"
# WHEN
response = self.client.post(
path=reverse("checkout-vv"),
format="json",
data={
"redirect_url": REDIRECT_URL,
"product": VV_DE_PRODUCT_SKU,
"address": {
"first_name": "Test",
"last_name": "User",
"street": "Test Street",
"street_number": "1",
"postal_code": "1234",
"city": "Test City",
"country": "209",
# NO company data
},
},
)
# THEN
self.assertEqual(response.status_code, status.HTTP_200_OK)
user = User.objects.get(username=USER_USERNAME)
self.assertEqual(user.invoice_address, User.INVOICE_ADDRESS_PRIVATE)
@patch("vbv_lernwelt.shop.views.init_transaction")
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
def test_checkout_happy_case(self, mock_init_transaction):
# GIVEN
mock_init_transaction.return_value = "1234567890"
@ -106,13 +76,12 @@ class CheckoutAPITestCase(APITestCase):
response.json()["next_step_url"],
)
self.assertTrue(
CheckoutInformation.objects.filter(
user=self.user,
product_sku=VV_DE_PRODUCT_SKU,
state=CheckoutState.ONGOING,
).exists()
)
ci = CheckoutInformation.objects.first()
self.assertEqual(ci.first_name, "Test")
self.assertEqual(ci.last_name, "User")
self.assertEqual(ci.country_id, "CH")
self.assertEqual(ci.state, "ongoing")
self.assertEqual(ci.transaction_id, "1234567890")
mock_init_transaction.assert_called_once_with(
user=self.user,
@ -123,13 +92,7 @@ class CheckoutAPITestCase(APITestCase):
webhook_url=f"{REDIRECT_URL}/api/shop/transaction/webhook/",
)
user = User.objects.get(username=USER_USERNAME)
self.assertEqual(user.street, TEST_ADDRESS_DATA["street"])
self.assertEqual(str(user.country.country_id), TEST_ADDRESS_DATA["country"])
self.assertEqual(user.invoice_address, User.INVOICE_ADDRESS_ORGANISATION)
@patch("vbv_lernwelt.shop.views.init_transaction")
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
def test_incomplete_setup(self, mock_init_transaction):
# GIVEN
Product.objects.all().delete()
@ -156,7 +119,7 @@ class CheckoutAPITestCase(APITestCase):
self.assertEqual(expected, response.json()["next_step_url"])
@patch("vbv_lernwelt.shop.views.init_transaction")
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
def test_checkout_init_transaction_exception(self, mock_init_transaction):
# GIVEN
mock_init_transaction.side_effect = InitTransactionException(
@ -213,7 +176,7 @@ class CheckoutAPITestCase(APITestCase):
response.json()["next_step_url"],
)
@patch("vbv_lernwelt.shop.views.init_transaction")
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
def test_checkout_double_checkout(self, mock_init_transaction):
"""Advise by Datatrans: Just create a new transaction."""
# GIVEN
@ -277,7 +240,7 @@ class CheckoutAPITestCase(APITestCase):
).exists()
)
@patch("vbv_lernwelt.shop.views.init_transaction")
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
def test_checkout_failed_creates_new(self, mock_init_transaction):
# GIVEN
state = CheckoutState.FAILED
@ -310,7 +273,7 @@ class CheckoutAPITestCase(APITestCase):
response.json()["next_step_url"],
)
@patch("vbv_lernwelt.shop.views.init_transaction")
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
def test_checkout_cancelled_creates_new(self, mock_init_transaction):
# GIVEN
state = CheckoutState.CANCELED

View File

@ -6,7 +6,7 @@ from django.test import override_settings, TestCase
from vbv_lernwelt.core.models import User
from vbv_lernwelt.shop.services import (
get_payment_url,
init_transaction,
init_datatrans_transaction,
InitTransactionException,
)
@ -36,7 +36,7 @@ class DatatransServiceTest(TestCase):
self.user.language = "it"
# WHEN
transaction_id = init_transaction(
transaction_id = init_datatrans_transaction(
user=self.user,
amount_chf_centimes=324_30,
redirect_url_success=f"{REDIRECT_URL}/success",
@ -76,7 +76,7 @@ class DatatransServiceTest(TestCase):
# WHEN / THEN
with self.assertRaises(InitTransactionException):
init_transaction(
init_datatrans_transaction(
user=self.user,
amount_chf_centimes=324_30,
redirect_url_success=f"/success",

View File

@ -5,6 +5,7 @@ from rest_framework import status
from rest_framework.test import APITestCase
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.core.model_utils import add_countries
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
from vbv_lernwelt.course.creators.test_utils import create_course, create_course_session
from vbv_lernwelt.course.models import CourseSessionUser
@ -29,6 +30,8 @@ def create_checkout_information(
class DatatransWebhookTestCase(APITestCase):
def setUp(self) -> None:
add_countries(small_set=True)
course, _ = create_course(
title="VV_in_DE",
# needed for VV_DE_PRODUCT_SKU
@ -102,13 +105,13 @@ class DatatransWebhookTestCase(APITestCase):
checkout_info.street_number = "1"
checkout_info.postal_code = "1234"
checkout_info.city = "Musterstadt"
checkout_info.country = "Schweiz"
checkout_info.company_name = "Musterfirma"
checkout_info.company_street = "Firmastrasse"
checkout_info.company_street_number = "2"
checkout_info.company_postal_code = "5678"
checkout_info.company_city = "Firmastadt"
checkout_info.company_country = "Schweiz"
checkout_info.country_id = "CH"
checkout_info.organisation_detail_name = "Musterfirma"
checkout_info.organisation_street = "Firmastrasse"
checkout_info.organisation_street_number = "2"
checkout_info.organisation_postal_code = "5678"
checkout_info.organisation_city = "Firmastadt"
checkout_info.organisation_country_id = "CH"
checkout_info.save()
mock_is_signature_valid.return_value = True
@ -181,10 +184,10 @@ class DatatransWebhookTestCase(APITestCase):
"target_url": "https://my.vbv-afa.ch/",
"name": "Max Mustermann",
"private_street": "Musterstrasse 1",
"private_city": "1234 Musterstadt Schweiz",
"private_city": "CH-1234 Musterstadt",
"company_name": "Musterfirma",
"company_street": "Firmastrasse 2",
"company_city": "5678 Firmastadt Schweiz",
"company_city": "CH-5678 Firmastadt",
},
template_language=self.user.language,
fail_silently=ANY,

View File

@ -5,6 +5,7 @@ from django.test import TestCase
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.model_utils import add_countries
from vbv_lernwelt.shop.invoice.abacus import (
AbacusInvoiceCreator,
create_customer_xml,
@ -23,6 +24,7 @@ USER_PASSWORD = "testpassword"
class AbacusInvoiceTestCase(TestCase):
def setUp(self):
add_countries(small_set=True)
create_default_users()
def test_create_invoice_xml(self):
@ -136,13 +138,14 @@ class AbacusInvoiceTestCase(TestCase):
street_number="32",
postal_code="1719",
city="Zumholz",
country="209",
company_name="VBV",
company_street="Laupenstrasse",
company_street_number="10",
company_postal_code="3000",
company_city="Bern",
company_country="209",
country_id="CH",
invoice_address="org",
organisation_detail_name="VBV",
organisation_street="Laupenstrasse",
organisation_street_number="10",
organisation_postal_code="3000",
organisation_city="Bern",
organisation_country_id="CH",
)
feuz_checkout_info.created_at = datetime(2024, 2, 15, 8, 33, 12, 0)
@ -158,10 +161,11 @@ class AbacusInvoiceTestCase(TestCase):
"<Email>andreas.feuz@eiger-versicherungen.ch</Email>"
in customer_xml_content
)
assert "<AddressNumber>60000012</AddressNumber>" in customer_xml_content
assert "<Name>Feuz</Name>" in customer_xml_content
assert "<Text>VBV</Text>" in customer_xml_content
# FIXME refactor country
assert "<Country>209</Country>" in customer_xml_content
assert "<Street>Laupenstrasse</Street>" in customer_xml_content
assert "<Country>CH</Country>" in customer_xml_content
def test_render_customer_xml(self):
customer_xml = render_customer_xml(

View File

@ -1,10 +1,8 @@
import structlog
from django.conf import settings
from django.http import JsonResponse
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from sentry_sdk import capture_exception
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
@ -18,7 +16,7 @@ from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState, Product
from vbv_lernwelt.shop.services import (
datatrans_state_to_checkout_state,
get_payment_url,
init_transaction,
init_datatrans_transaction,
InitTransactionException,
is_signature_valid,
)
@ -109,7 +107,7 @@ def checkout_vv(request):
return next_step_response(url="/")
try:
transaction_id = init_transaction(
transaction_id = init_datatrans_transaction(
user=request.user,
amount_chf_centimes=product.price,
redirect_url_success=checkout_success_url(
@ -176,10 +174,10 @@ def send_vv_welcome_email(checkout_info: CheckoutInformation):
"target_url": "https://my.vbv-afa.ch/",
"name": f"{checkout_info.first_name} {checkout_info.last_name}",
"private_street": f"{checkout_info.street} {checkout_info.street_number}",
"private_city": f"{checkout_info.postal_code} {checkout_info.city} {checkout_info.country}",
"company_name": checkout_info.company_name,
"company_street": f"{checkout_info.company_street} {checkout_info.company_street_number}",
"company_city": f"{checkout_info.company_postal_code} {checkout_info.company_city} {checkout_info.company_country}",
"private_city": f"{checkout_info.country_id}-{checkout_info.postal_code} {checkout_info.city}",
"company_name": checkout_info.organisation_detail_name,
"company_street": f"{checkout_info.organisation_street} {checkout_info.organisation_street_number}",
"company_city": f"{checkout_info.organisation_country_id}-{checkout_info.organisation_postal_code} {checkout_info.organisation_city}",
},
template_language=checkout_info.user.language,
fail_silently=True,