Repository: stellar/django-polaris
Pull request #662 (2022-11-22 20:21:30)

Update SEP-24 & SEP-6 `lang` support
Updates SEP-6 & SEP-24's support for the `lang` parameter, which was recently updated to default to `en` even when the value passed is not supported, instead of returning 400.


Patch:
diff --git a/polaris/locale/utils.py b/polaris/locale/utils.py
index a4bcc58..fb9e2c2 100644
--- a/polaris/locale/utils.py
+++ b/polaris/locale/utils.py
@@ -1,23 +1,15 @@
-from typing import Optional
-
 from django.utils import translation
-from django.utils.translation import gettext as _, get_supported_language_variant
-from rest_framework.response import Response
-
-from polaris.utils import render_error_response
+from django.utils.translation import get_supported_language_variant
 
 
-def validate_language(lang: str, as_html: bool = False) -> Optional[Response]:
+def validate_or_use_default_language(lang: str) -> str:
     if not lang:
-        return render_error_response(
-            _("missing language code in request"), as_html=as_html
-        )
+        return "en"
     try:
         get_supported_language_variant(lang)
     except LookupError:
-        return render_error_response(
-            _("unsupported language: %s" % lang), as_html=as_html
-        )
+        return "en"
+    return lang
 
 
 def activate_lang_for_request(lang: str):
diff --git a/polaris/sep24/deposit.py b/polaris/sep24/deposit.py
index 6ad3c73..801aec4 100644
--- a/polaris/sep24/deposit.py
+++ b/polaris/sep24/deposit.py
@@ -49,7 +49,10 @@ from polaris.sep24.utils import (
 )
 from polaris.models import Asset, Transaction
 from polaris.integrations.forms import TransactionForm
-from polaris.locale.utils import validate_language, activate_lang_for_request
+from polaris.locale.utils import (
+    activate_lang_for_request,
+    validate_or_use_default_language,
+)
 from polaris.integrations import (
     registered_deposit_integration as rdi,
     registered_fee_func,
@@ -422,7 +425,10 @@ def deposit(token: SEP10Token, request: Request) -> Response:
     destination_account = (
         request.data.get("account") or token.muxed_account or token.account
     )
-    lang = request.data.get("lang")
+
+    lang = validate_or_use_default_language(request.data.get("lang"))
+    activate_lang_for_request(lang)
+
     sep9_fields = extract_sep9_fields(request.data)
     claimable_balance_supported = request.data.get("claimable_balance_supported")
     if not claimable_balance_supported:
@@ -440,12 +446,6 @@ def deposit(token: SEP10Token, request: Request) -> Response:
             )
         )
 
-    if lang:
-        err_resp = validate_language(lang)
-        if err_resp:
-            return err_resp
-        activate_lang_for_request(lang)
-
     # Verify that the request is valid.
     if not asset_code:
         return render_error_response(_("`asset_code` is required"))
diff --git a/polaris/sep24/info.py b/polaris/sep24/info.py
index ceaa9ba..2c19679 100644
--- a/polaris/sep24/info.py
+++ b/polaris/sep24/info.py
@@ -5,10 +5,12 @@ from rest_framework.decorators import api_view, renderer_classes
 from rest_framework.response import Response
 from rest_framework.renderers import JSONRenderer, BrowsableAPIRenderer
 
+from polaris.locale.utils import (
+    activate_lang_for_request,
+    validate_or_use_default_language,
+)
 from polaris.models import Asset
 from polaris.integrations import (
-    registered_fee_func,
-    calculate_fee,
     registered_custody_integration as rci,
 )
 
@@ -41,7 +43,7 @@ def info(request):
     Definition of the /info endpoint, in accordance with SEP-0024.
     See: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#info
     """
-
+    activate_lang_for_request(validate_or_use_default_language(request.GET.get("lang")))
     info_data = {
         "deposit": {},
         "withdraw": {},
diff --git a/polaris/sep24/utils.py b/polaris/sep24/utils.py
index 2753ea5..eca2752 100644
--- a/polaris/sep24/utils.py
+++ b/polaris/sep24/utils.py
@@ -22,7 +22,10 @@ from django.conf import settings as django_settings
 from django.core.validators import URLValidator
 from stellar_sdk import MuxedAccount
 
-from polaris.locale.utils import validate_language, activate_lang_for_request
+from polaris.locale.utils import (
+    activate_lang_for_request,
+    validate_or_use_default_language,
+)
 from polaris import settings
 from polaris.utils import getLogger
 from polaris.models import Asset, Transaction
@@ -225,7 +228,8 @@ def interactive_args_validation(request: Request, kind: str) -> Dict:
     callback = request.GET.get("callback")
     on_change_callback = request.GET.get("on_change_callback")
     amount_str = request.GET.get("amount")
-    lang = request.GET.get("lang")
+    lang = validate_or_use_default_language(request.GET.get("lang"))
+    activate_lang_for_request(lang)
     asset = Asset.objects.filter(code=asset_code, sep24_enabled=True).first()
     if not transaction_id:
         return dict(
@@ -240,11 +244,6 @@ def interactive_args_validation(request: Request, kind: str) -> Dict:
         for domain in settings.CALLBACK_REQUEST_DOMAIN_DENYLIST
     ):
         on_change_callback = None
-    if lang:
-        error_response = validate_language(lang, as_html=True)
-        if error_response:
-            return dict(error=error_response)
-        activate_lang_for_request(lang)
     try:
         transaction = Transaction.objects.get(id=transaction_id, asset=asset, kind=kind)
     except (ObjectDoesNotExist, ValidationError):
diff --git a/polaris/sep24/withdraw.py b/polaris/sep24/withdraw.py
index 7b16e01..1be9760 100644
--- a/polaris/sep24/withdraw.py
+++ b/polaris/sep24/withdraw.py
@@ -46,7 +46,10 @@ from polaris.sep10.utils import validate_sep10_token
 from polaris.sep10.token import SEP10Token
 from polaris.models import Asset, Transaction
 from polaris.integrations.forms import TransactionForm
-from polaris.locale.utils import validate_language, activate_lang_for_request
+from polaris.locale.utils import (
+    activate_lang_for_request,
+    validate_or_use_default_language,
+)
 from polaris.integrations import (
     registered_withdrawal_integration as rwi,
     registered_custody_integration as rci,
@@ -440,15 +443,12 @@ def withdraw(
     Creates an `incomplete` withdraw Transaction object in the database and
     returns the URL entry-point for the interactive flow.
     """
-    lang = request.data.get("lang")
+    lang = validate_or_use_default_language(request.data.get("lang"))
+    activate_lang_for_request(lang)
+
     asset_code = request.data.get("asset_code")
     source_account = request.data.get("account")
     sep9_fields = extract_sep9_fields(request.data)
-    if lang:
-        err_resp = validate_language(lang)
-        if err_resp:
-            return err_resp
-        activate_lang_for_request(lang)
     if not asset_code:
         return render_error_response(_("'asset_code' is required"))
 
diff --git a/polaris/sep6/deposit.py b/polaris/sep6/deposit.py
index f68d699..55b0a5e 100644
--- a/polaris/sep6/deposit.py
+++ b/polaris/sep6/deposit.py
@@ -21,7 +21,10 @@ from stellar_sdk.exceptions import (
 
 from polaris import settings
 from polaris.models import Asset, Transaction, Quote
-from polaris.locale.utils import validate_language, activate_lang_for_request
+from polaris.locale.utils import (
+    activate_lang_for_request,
+    validate_or_use_default_language,
+)
 from polaris.utils import (
     getLogger,
     render_error_response,
@@ -210,6 +213,9 @@ def validate_response(
 def parse_request_args(
     token: SEP10Token, request: Request, exchange: bool = False
 ) -> Dict:
+    lang = validate_or_use_default_language(request.GET.get("lang"))
+    activate_lang_for_request(lang)
+
     account = request.GET.get("account")
     if account.startswith("M"):
         try:
@@ -244,13 +250,6 @@ def parse_request_args(
             )
         }
 
-    lang = request.GET.get("lang")
-    if lang:
-        err_resp = validate_language(lang)
-        if err_resp:
-            return {"error": err_resp}
-        activate_lang_for_request(lang)
-
     memo_type = request.GET.get("memo_type")
     if memo_type and memo_type not in Transaction.MEMO_TYPES:
         return {"error": render_error_response(_("invalid 'memo_type'"))}
diff --git a/polaris/sep6/info.py b/polaris/sep6/info.py
index 91a9894..7024278 100644
--- a/polaris/sep6/info.py
+++ b/polaris/sep6/info.py
@@ -1,4 +1,9 @@
 from typing import Dict
+
+from polaris.locale.utils import (
+    activate_lang_for_request,
+    validate_or_use_default_language,
+)
 from polaris.utils import getLogger
 
 from django.utils.translation import gettext as _
@@ -31,9 +36,11 @@ def info(request: Request) -> Response:
         "transaction": {"enabled": True, "authentication_required": True},
         "features": {"account_creation": True, "claimable_balances": True},
     }
+    lang = validate_or_use_default_language(request.GET.get("lang"))
+    activate_lang_for_request(lang)
     error_response = None
     for asset in Asset.objects.filter(sep6_enabled=True):
-        error_response = populate_asset_info(request, asset, info_data, False)
+        error_response = populate_asset_info(request, asset, info_data, lang, False)
         if error_response:
             break
     if error_response:
@@ -45,7 +52,7 @@ def info(request: Request) -> Response:
     info_data["deposit-exchange"] = {}
     info_data["withdraw-exchange"] = {}
     for asset in Asset.objects.filter(sep6_enabled=True, sep38_enabled=True):
-        error_response = populate_asset_info(request, asset, info_data, True)
+        error_response = populate_asset_info(request, asset, info_data, lang, True)
         if error_response:
             break
     if error_response:
@@ -54,16 +61,13 @@ def info(request: Request) -> Response:
     return Response(info_data)
 
 
-def populate_asset_info(request, asset, info_data, exchange=False):
-    try:
-        fields_and_types = registered_info_func(
-            request=request,
-            asset=asset,
-            lang=request.GET.get("lang"),
-            exchange=exchange,
-        )
-    except ValueError:
-        return render_error_response("unsupported 'lang'")
+def populate_asset_info(request, asset, info_data, lang, exchange=False):
+    fields_and_types = registered_info_func(
+        request=request,
+        asset=asset,
+        lang=lang,
+        exchange=exchange,
+    )
     try:
         validate_integration(fields_and_types)
     except ValueError as e:
diff --git a/polaris/sep6/withdraw.py b/polaris/sep6/withdraw.py
index 1307543..c8583ec 100644
--- a/polaris/sep6/withdraw.py
+++ b/polaris/sep6/withdraw.py
@@ -32,7 +32,10 @@ from polaris.sep6.utils import validate_403_response
 from polaris.sep10.token import SEP10Token
 from polaris.sep10.utils import validate_sep10_token
 from polaris.shared.endpoints import SEP6_MORE_INFO_PATH
-from polaris.locale.utils import validate_language, activate_lang_for_request
+from polaris.locale.utils import (
+    activate_lang_for_request,
+    validate_or_use_default_language,
+)
 from polaris.models import Asset, Transaction, Quote
 from polaris.integrations import (
     registered_withdrawal_integration as rwi,
@@ -153,6 +156,9 @@ def withdraw_logic(token: SEP10Token, request: Request, exchange: bool):
 def parse_request_args(
     token: SEP10Token, request: Request, exchange: bool = False
 ) -> Dict:
+    lang = validate_or_use_default_language(request.GET.get("lang"))
+    activate_lang_for_request(lang)
+
     account = request.GET.get("account")
     if account and account.startswith("M"):
         try:
@@ -185,13 +191,6 @@ def parse_request_args(
             "error": render_error_response(_("invalid 'asset_code' or 'source_asset'"))
         }
 
-    lang = request.GET.get("lang")
-    if lang:
-        err_resp = validate_language(lang)
-        if err_resp:
-            return {"error": err_resp}
-        activate_lang_for_request(lang)
-
     memo_type = request.GET.get("memo_type")
     if memo_type and memo_type not in Transaction.MEMO_TYPES:
         return {"error": render_error_response(_("invalid 'memo_type'"))}
@@ -264,7 +263,7 @@ def parse_request_args(
         "source_asset" if exchange else "asset": asset,
         "memo_type": memo_type,
         "memo": request.GET.get("memo"),
-        "lang": request.GET.get("lang"),
+        "lang": lang,
         "type": request.GET.get("type"),
         "dest": request.GET.get("dest"),
         "dest_extra": request.GET.get("dest_extra"),
diff --git a/polaris/shared/endpoints.py b/polaris/shared/endpoints.py
index 031a873..9ca8255 100644
--- a/polaris/shared/endpoints.py
+++ b/polaris/shared/endpoints.py
@@ -9,6 +9,10 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist
 from django.utils.translation import gettext as _
 from django.conf import settings as django_settings
 
+from polaris.locale.utils import (
+    activate_lang_for_request,
+    validate_or_use_default_language,
+)
 from polaris import settings as polaris_settings
 from polaris.templates import Template
 from polaris.utils import render_error_response, getLogger
@@ -208,6 +212,8 @@ def transactions_request(
     token: SEP10Token,
     sep6: bool = False,
 ) -> Response:
+    activate_lang_for_request(validate_or_use_default_language(request.GET.get("lang")))
+
     try:
         limit = _validate_limit(request.GET.get("limit"))
     except ValueError:
@@ -266,6 +272,7 @@ def transaction_request(
     token: SEP10Token,
     sep6: bool = False,
 ) -> Response:
+    activate_lang_for_request(validate_or_use_default_language(request.GET.get("lang")))
     try:
         request_transaction = _get_transaction_from_request(
             request,


Test patch:
diff --git a/polaris/tests/sep6/test_deposit.py b/polaris/tests/sep6/test_deposit.py
index ff58a8c..ce3ba9b 100644
--- a/polaris/tests/sep6/test_deposit.py
+++ b/polaris/tests/sep6/test_deposit.py
@@ -934,8 +934,8 @@ def test_deposit_bad_lang(mock_process_sep6_request, client):
             "lang": "es",
         },
     )
-    assert response.status_code == 400, response.content
-    assert response.json() == {"error": "unsupported language: es"}
+    assert response.status_code == 200, response.content
+    assert response.headers.get("Content-Language") == "en"
 
 
 @pytest.mark.django_db
diff --git a/polaris/tests/sep6/test_info.py b/polaris/tests/sep6/test_info.py
index b27b11f..4cfd75e 100644
--- a/polaris/tests/sep6/test_info.py
+++ b/polaris/tests/sep6/test_info.py
@@ -233,15 +233,10 @@ def server_error(client, usd_asset_factory):
     assert content == {"error": "unable to process the request"}
 
 
-def unsupported_lang(request, asset, lang, *args, **kwargs):
-    raise ValueError()
-
-
 @pytest.mark.django_db
-@patch("polaris.sep6.info.registered_info_func", unsupported_lang)
+@patch("polaris.sep6.info.registered_info_func", good_info_integration)
 def test_unsupported_lang(client, usd_asset_factory):
     usd_asset_factory(protocols=[Transaction.PROTOCOL.sep6])
     response = client.get(INFO_PATH + "?lang=es")
-    content = json.loads(response.content)
-    assert response.status_code == 400, response.content
-    assert content == {"error": "unsupported 'lang'"}
+    assert response.status_code == 200, response.content
+    assert response.headers.get("Content-Language") == "en"
diff --git a/polaris/tests/sep6/test_withdraw.py b/polaris/tests/sep6/test_withdraw.py
index 63f0cc3..57af774 100644
--- a/polaris/tests/sep6/test_withdraw.py
+++ b/polaris/tests/sep6/test_withdraw.py
@@ -670,6 +670,7 @@ def test_withdraw_bad_lang(mock_process_sep6_request, client):
     asset = Asset.objects.create(
         code="USD",
         issuer=Keypair.random().public_key,
+        distribution_seed=Keypair.random().secret,
         withdrawal_min_amount=10,
         withdrawal_max_amount=1000,
         sep6_enabled=True,
@@ -690,8 +691,8 @@ def test_withdraw_bad_lang(mock_process_sep6_request, client):
             "dest": "test bank account number",
         },
     )
-    assert response.status_code == 400, response.content
-    assert response.json() == {"error": "unsupported language: es"}
+    assert response.status_code == 200, response.content
+    assert response.headers.get("Content-Language") == "en"
 
 
 @pytest.mark.django_db