Skip to content

Commit

Permalink
imto
Browse files Browse the repository at this point in the history
  • Loading branch information
domdinicola committed Jan 25, 2025
1 parent 7cfca34 commit f78859b
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src/aurora/config/fragments/constance.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
"choices": (("html", "HTML"), ("line", "NEWLINE"), ("space", "SPACES")),
},
],
"write_only_input": [
"django.forms.fields.CharField",
{
"required": False,
"widget": "aurora.core.constance.WriteOnlyInput",
},
],
}
CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend"
CONSTANCE_DATABASE_CACHE_BACKEND = env("CONSTANCE_DATABASE_CACHE_BACKEND")
Expand Down Expand Up @@ -43,5 +50,7 @@
str,
),
"WAF_ADMIN_ALLOWED_HOSTNAMES": ("", "admin website hostname (regex)", str),
"IMTO_NAME_ENQUIRY_URL": ("", "IMTO Name Enquiry Service URL", str),
"IMTO_TOKEN": ("", "IMTO Service Token", "write_only_input"),
}
)
54 changes: 54 additions & 0 deletions src/aurora/core/constance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import logging
from typing import Any

from django.forms import ChoiceField, HiddenInput, Textarea, TextInput
from django.template import Context, Template
from django.utils.safestring import SafeString, mark_safe

from constance import config

logger = logging.getLogger(__name__)


class ObfuscatedInput(HiddenInput):
def render(
self,
name: str,
value: Any,
attrs: dict[str, str] | None = None,
renderer: Any | None = None,
) -> "SafeString":
context = self.get_context(name, value, attrs)
context["value"] = str(value)
context["label"] = "Set" if value else "Not Set"

Check warning on line 23 in src/aurora/core/constance.py

View check run for this annotation

Codecov / codecov/patch

src/aurora/core/constance.py#L21-L23

Added lines #L21 - L23 were not covered by tests

tpl = Template('<input type="hidden" name="{{ widget.name }}" value="{{ value }}">{{ label }}')
return mark_safe(tpl.render(Context(context))) # noqa: S308

Check warning

Code scanning / Bandit

Potential XSS on mark_safe function. Warning

Potential XSS on mark_safe function.

Check warning

Code scanning / Bandit

Use of mark_safe() may expose cross-site scripting vulnerabilities and should be reviewed. Warning

Use of mark_safe() may expose cross-site scripting vulnerabilities and should be reviewed.

Check warning on line 26 in src/aurora/core/constance.py

View check run for this annotation

Codecov / codecov/patch

src/aurora/core/constance.py#L25-L26

Added lines #L25 - L26 were not covered by tests


class WriteOnlyWidget:
def format_value(self, value: Any) -> str:
return super().format_value("***")

Check warning on line 31 in src/aurora/core/constance.py

View check run for this annotation

Codecov / codecov/patch

src/aurora/core/constance.py#L31

Added line #L31 was not covered by tests

def value_from_datadict(self, data: dict[str, Any], files: Any, name: str) -> Any:
value = data.get(name)

Check warning on line 34 in src/aurora/core/constance.py

View check run for this annotation

Codecov / codecov/patch

src/aurora/core/constance.py#L34

Added line #L34 was not covered by tests
if value == "***":
return getattr(config, name)
return value

Check warning on line 37 in src/aurora/core/constance.py

View check run for this annotation

Codecov / codecov/patch

src/aurora/core/constance.py#L36-L37

Added lines #L36 - L37 were not covered by tests


class WriteOnlyTextarea(WriteOnlyWidget, Textarea):
pass


class WriteOnlyInput(WriteOnlyWidget, TextInput):
pass


class GroupChoiceField(ChoiceField):
def __init__(self, **kwargs: Any) -> None:
from django.contrib.auth.models import Group

Check warning on line 50 in src/aurora/core/constance.py

View check run for this annotation

Codecov / codecov/patch

src/aurora/core/constance.py#L50

Added line #L50 was not covered by tests

ret: list[tuple[str | int, str]] = [(c["name"], c["name"]) for c in Group.objects.values("pk", "name")]
kwargs["choices"] = ret
super().__init__(**kwargs)

Check warning on line 54 in src/aurora/core/constance.py

View check run for this annotation

Codecov / codecov/patch

src/aurora/core/constance.py#L52-L54

Added lines #L52 - L54 were not covered by tests
1 change: 1 addition & 0 deletions src/aurora/core/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .label import LabelOnlyField # noqa
from .mixins import SmartFieldMixin # noqa
from .multi_checkbox import MultiCheckboxField # noqa
from .imto import IMTONameEnquiryField # noqa
from .radio import RadioField, YesNoChoice, YesNoRadio # noqa
from .remote_ip import RemoteIpField # noqa
from .selected import AjaxSelectField, SelectField, SmartSelectWidget # noqa
Expand Down
64 changes: 64 additions & 0 deletions src/aurora/core/fields/imto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import requests
from constance import config
from django import forms
from django.core.exceptions import ValidationError
from django.forms import MultiWidget
from django.forms.widgets import TextInput


# https://documenter.getpostman.com/view/20828158/2s93sW7aEc#4fda8a6a-0c21-42b2-a86b-03dce41303ad
class IMTONameEnquiryMultiWidget(MultiWidget):
def __init__(self, *args, **kwargs):
self.widgets = (
TextInput({"placeholder": "Bank Code"}),
TextInput({"placeholder": "Account Number"}),
TextInput({"placeholder": "Account Full Name"}),
)
super().__init__(self.widgets, *args, **kwargs)

def decompress(self, value):
return value.rsplit("|") if value else [None, None, None]

Check warning on line 20 in src/aurora/core/fields/imto.py

View check run for this annotation

Codecov / codecov/patch

src/aurora/core/fields/imto.py#L20

Added line #L20 was not covered by tests


class IMTONameEnquiryField(forms.fields.MultiValueField):
widget = IMTONameEnquiryMultiWidget

def __init__(self, *args, **kwargs):
list_fields = [forms.fields.CharField(max_length=16), forms.fields.CharField(max_length=32)]
super().__init__(list_fields, *args, **kwargs)

def compress(self, values):
return "|".join(values)

Check warning on line 31 in src/aurora/core/fields/imto.py

View check run for this annotation

Codecov / codecov/patch

src/aurora/core/fields/imto.py#L31

Added line #L31 was not covered by tests

def validate(self, value):
super().validate(value)
try:
account_number, bank_code, account_full_name = value.rsplit("|")
except ValueError:
raise ValidationError("ValueError: not enough values to unpack")

headers = {
"x-token": config.IMTO_TOKEN,
}
body = {"accountNumber": account_number, "bankCode": bank_code}
response = requests.post(config.IMTO_NAME_ENQUIRY_URL, headers=headers, json=body, timeout=60)

if (
response.status_code == 200
and not response.json()["error"]
and response.json()["code"] == "00"
and response.json()["data"]["beneficiaryAccountName"] == account_full_name
):
return

if response.status_code == 500:
message = response.reason
elif response.json()["error"]:
message = response.json()["error"]
elif 300 <= response.status_code < 500:
message = f"Error {response.status_code}"
elif response.json()["data"]["beneficiaryAccountName"] != account_full_name:
message = f"Wrong account holder {account_full_name}"
else:
message = "Generic Error"

Check warning on line 63 in src/aurora/core/fields/imto.py

View check run for this annotation

Codecov / codecov/patch

src/aurora/core/fields/imto.py#L63

Added line #L63 was not covered by tests
raise ValidationError(message)
1 change: 1 addition & 0 deletions src/aurora/core/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def __contains__(self, y):
field_registry.register(fields.HiddenField)
field_registry.register(MathCaptchaField)
field_registry.register(fields.CaptchaField)
field_registry.register(fields.IMTONameEnquiryField)

form_registry = Registry(forms.BaseForm)

Expand Down
122 changes: 122 additions & 0 deletions tests/fields/test_imto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from unittest.mock import Mock, patch

import pytest
from django.core.exceptions import ValidationError

from aurora.core.fields import IMTONameEnquiryField


@pytest.mark.django_db
@patch("aurora.core.fields.imto.requests.post")
def test_imto_name_enquiry_ok(mock_post):
mock_post.return_value = Mock(
status_code=200,
json=lambda: {
"message": "operation successful",
"data": {
"lookupParam": "100001241210170958777131925463_DANIEL ADEBAYO ADEDOYIN_22000000051_3",
"beneficiaryAccountNumber": "8168208035",
"beneficiaryBankCode": "100004",
"beneficiaryAccountName": "DANIEL ADEBAYO ADEDOYIN",
},
"code": "00",
"error": "",
},
)
fld = IMTONameEnquiryField()
assert fld.validate("bank|account|DANIEL ADEBAYO ADEDOYIN") is None


@pytest.mark.django_db
@patch("aurora.core.fields.imto.requests.post")
def test_imto_name_enquiry_ko_not_matching_name(mock_post):
mock_post.return_value = Mock(
status_code=200,
json=lambda: {
"message": "operation successful",
"data": {
"lookupParam": "100001241210170958777131925463_DANIEL ADEBAYO ADEDOYIN_22000000051_3",
"beneficiaryAccountNumber": "8168208035",
"beneficiaryBankCode": "100004",
"beneficiaryAccountName": "DANIEL ADEBAYO ADEDOYIN",
},
"code": "00",
"error": "",
},
)
fld = IMTONameEnquiryField()
with pytest.raises(ValidationError, match="Wrong account holder mimmo"):
assert fld.validate("bank|account|mimmo") is None


def test_imto_name_enquiry_ko_value_error():
fld = IMTONameEnquiryField()
with pytest.raises(ValidationError, match="ValueError: not enough values to unpack"):
fld.validate("only_one_value")


@pytest.mark.django_db
@patch("aurora.core.fields.imto.requests.post")
def test_imto_name_enquiry_error_status_code(mock_post):
mock_post.return_value = Mock(
status_code=400,
json=lambda: {
"message": "operation successful",
"data": {
"lookupParam": "100001241210170958777131925463_DANIEL ADEBAYO ADEDOYIN_22000000051_3",
"beneficiaryAccountNumber": "8168208035",
"beneficiaryBankCode": "100004",
"beneficiaryAccountName": "DANIEL ADEBAYO ADEDOYIN",
},
"code": "00",
"error": "",
},
)
fld = IMTONameEnquiryField()
with pytest.raises(ValidationError, match="Error 400"):
assert fld.validate("bank|account|mimmo") is None


@pytest.mark.django_db
@patch("aurora.core.fields.imto.requests.post")
def test_imto_name_enquiry_error_500(mock_post):
mock_post.return_value = Mock(
status_code=500,
reason="Internal Server Error",
json=lambda: {
"message": "operation successful",
"data": {
"lookupParam": "100001241210170958777131925463_DANIEL ADEBAYO ADEDOYIN_22000000051_3",
"beneficiaryAccountNumber": "8168208035",
"beneficiaryBankCode": "100004",
"beneficiaryAccountName": "DANIEL ADEBAYO ADEDOYIN",
},
"code": "00",
"error": "",
},
)
fld = IMTONameEnquiryField()
with pytest.raises(ValidationError, match="Internal Server Error"):
assert fld.validate("bank|account|mimmo") is None


@pytest.mark.django_db
@patch("aurora.core.fields.imto.requests.post")
def test_imto_name_enquiry_error_error(mock_post):
mock_post.return_value = Mock(
status_code=400,
json=lambda: {
"message": "operation successful",
"data": {
"lookupParam": "100001241210170958777131925463_DANIEL ADEBAYO ADEDOYIN_22000000051_3",
"beneficiaryAccountNumber": "8168208035",
"beneficiaryBankCode": "100004",
"beneficiaryAccountName": "DANIEL ADEBAYO ADEDOYIN",
},
"code": "00",
"error": "Bad Request",
},
)
fld = IMTONameEnquiryField()
with pytest.raises(ValidationError, match="Bad Request"):
assert fld.validate("bank|account|mimmo") is None

0 comments on commit f78859b

Please sign in to comment.