Merge pull request #10 from dethos/bitid

Basic BitId support
This commit is contained in:
Gonçalo Valério 2020-03-31 15:44:42 +01:00 committed by GitHub
commit 197fb789cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 443 additions and 61 deletions

View File

@ -7,6 +7,8 @@ History
++++++++++++++
* Add validation for existing addresses on the signup form
* Add rudimentary BitId support
* Renamed the base auth views to generic names
0.0.2 (2020-01-08)
++++++++++++++++++

View File

@ -11,7 +11,10 @@ Django-Cryptolock
.. image:: https://coveralls.io/repos/github/dethos/django-cryptolock/badge.svg
:target: https://coveralls.io/github/dethos/django-cryptolock
Django authentication using cryptocurrency wallets
Django authentication using cryptocurrency wallets.
**DISCLAIMER:** This package is still in an early stage of development. It isn't meant to be
used on any production scenario yet (in other words, only test projects for now).
Documentation
-------------
@ -51,7 +54,7 @@ Add Django-Cryptolock's URL patterns:
]
Add the following settings to your project:
Add the following settings to your project for the Monero Backend:
* ``django_cryptolock.backends.MoneroAddressBackend`` to your
``AUTHENTICATION_BACKENDS``
@ -61,6 +64,13 @@ Add the following settings to your project:
``DJCL_MONERO_WALLET_RPC_USER`` and ``DJCL_MONERO_WALLET_RPC_PASS`` to specify
which wallet RPC should be used.
In case only Bitcoin Backend is used, you just need:
* ``DJCL_BITCOIN_NETWORK`` with
one of two possible values: ``mainnet`` or ``testnet``.
* Add ``django_cryptolock.backends.BitcoinAddressBackend`` to your
``AUTHENTICATION_BACKENDS``
Finaly create the templates files (``login.html`` and ``signup.html``) under a
``django_cryptolock`` subfolder.

View File

@ -2,11 +2,12 @@ from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from monerorpc.authproxy import AuthServiceProxy, JSONRPCException
from .models import Address
from .utils import verify_signature
from .utils import verify_monero_signature, verify_bitcoin_signature
User = get_user_model()
@ -27,12 +28,16 @@ class MoneroAddressBackend(ModelBackend):
return None
stored_address = (
Address.objects.select_related("user").filter(address=address).first()
Address.objects.select_related("user")
.filter(address=address, network=Address.NETWORK_MONERO)
.first()
)
if not stored_address:
return None
try:
is_valid = verify_signature(address, challenge, signature)
is_valid = verify_monero_signature(
stored_address.address, challenge, signature
)
except JSONRPCException:
raise PermissionDenied(_("Error while validating signature"))
@ -40,3 +45,35 @@ class MoneroAddressBackend(ModelBackend):
return stored_address.user
return None
class BitcoinAddressBackend(ModelBackend):
"""Custom Bitcoin-BitId authentication backend."""
def authenticate(
self, request, address=None, challenge=None, signature=None, **kwargs
):
"""
Validates the provided signature for the given Bitcoin address and challenge.
This method does not rely on any external components, everything is done locally.
"""
if not all([address, challenge, signature]):
return None
stored_address = (
Address.objects.select_related("user")
.filter(address=address, network=Address.NETWORK_BITCOIN)
.first()
)
if not stored_address:
return None
valid_signature = verify_bitcoin_signature(
stored_address.address, challenge, signature, request
)
if valid_signature:
return stored_address.user
else:
return None

View File

@ -1,9 +1,14 @@
from django import forms
from django.contrib.auth import authenticate
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.utils.translation import gettext, gettext_lazy as _
from django.conf import settings
from pybitid import bitid
from .models import Address
from .validators import validate_monero_address
from .validators import validate_monero_address, validate_bitcoin_address
from .utils import generate_challenge
@ -17,7 +22,9 @@ class ChallengeMixin(forms.Form):
challenge = forms.CharField()
def include_challange(self):
new_challenge = generate_challenge()
new_challenge = bitid.build_uri(
self.request.build_absolute_uri(), generate_challenge()
)
if not self.data:
self.request.session["current_challenge"] = new_challenge
self.initial["challenge"] = new_challenge
@ -33,7 +40,7 @@ class ChallengeMixin(forms.Form):
class SimpleLoginForm(ChallengeMixin, forms.Form):
"""Basic login form, that can be used as reference for implementation."""
address = forms.CharField(validators=[validate_monero_address])
address = forms.CharField()
signature = forms.CharField()
error_messages = {
@ -84,7 +91,7 @@ class SimpleSignUpForm(ChallengeMixin, forms.Form):
"""Basic login form, that can be used as reference for implementation."""
username = forms.CharField()
address = forms.CharField(validators=[validate_monero_address])
address = forms.CharField()
signature = forms.CharField()
def __init__(self, request=None, *args, **kwargs):
@ -93,9 +100,38 @@ class SimpleSignUpForm(ChallengeMixin, forms.Form):
super().__init__(*args, **kwargs)
self.request = request
self.include_challange()
self.network = None
def clean_address(self):
self.network = None
value = self.cleaned_data["address"]
bitcoin_backend = "django_cryptolock.backends.BitcoinAddressBackend"
monero_backend = "django_cryptolock.backends.MoneroAddressBackend"
if bitcoin_backend in settings.AUTHENTICATION_BACKENDS:
try:
validate_bitcoin_address(value)
self.network = Address.NETWORK_BITCOIN
except ValidationError:
pass
if monero_backend in settings.AUTHENTICATION_BACKENDS:
try:
validate_monero_address(value)
self.network = Address.NETWORK_MONERO
except ValidationError:
pass
if not self.network:
raise forms.ValidationError(_("Invalid address"))
if Address.objects.filter(address=value).exists():
raise forms.ValidationError(_("This address already exists"))
return value
def clean_username(self):
value = self.cleaned_data["username"]
if get_user_model().objects.filter(username=value).exists():
raise forms.ValidationError(_("This username is already taken"))
return value

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.5 on 2020-02-18 19:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("django_cryptolock", "0001_initial")]
operations = [
migrations.AddField(
model_name="address",
name="network",
field=models.PositiveSmallIntegerField(
choices=[(1, "Monero"), (2, "Bitcoin")], default=1
),
),
migrations.AlterField(
model_name="address",
name="address",
field=models.CharField(max_length=106, unique=True),
),
]

View File

@ -1,21 +1,28 @@
# -*- coding: utf-8 -*-
from django.db import models
from django.http.request import HttpRequest
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from model_utils.models import TimeStampedModel
from .validators import validate_monero_address
from .validators import validate_monero_address, validate_bitcoin_address
from .utils import verify_bitcoin_signature, verify_monero_signature
class Address(TimeStampedModel):
"""Addresses that belong to a given user account."""
NETWORK_MONERO = 1
NETWORK_BITCOIN = 2
NETWORKS = ((NETWORK_MONERO, "Monero"), (NETWORK_BITCOIN, "Bitcoin"))
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
address = models.CharField(
max_length=106, validators=[validate_monero_address], unique=True
)
network = models.PositiveSmallIntegerField(choices=NETWORKS, default=NETWORK_MONERO)
address = models.CharField(max_length=106, unique=True)
class Meta:
"""Meta definition for Address."""
@ -26,3 +33,12 @@ class Address(TimeStampedModel):
def __str__(self):
"""Unicode representation of Address."""
return self.address
def clean(self):
try:
if self.network == self.NETWORK_MONERO:
validate_monero_address(self.address)
else:
validate_bitcoin_address(self.address)
except ValidationError:
raise ValidationError(_("Invalid address for the given network"))

View File

@ -2,11 +2,11 @@
from django.conf.urls import url
from django.views.generic import TemplateView
from .views import MoneroLoginView, MoneroSignUpView
from .views import CryptoLockLoginView, CryptoLockSignUpView
app_name = "django_cryptolock"
urlpatterns = [
url(r"login", MoneroLoginView.as_view(), name="login"),
url(r"signup", MoneroSignUpView.as_view(), name="signup"),
url(r"login", CryptoLockLoginView.as_view(), name="login"),
url(r"signup", CryptoLockSignUpView.as_view(), name="signup"),
]

View File

@ -1,11 +1,15 @@
import warnings
from secrets import token_hex
from django.conf import settings
from django.http.request import HttpRequest
from django.utils.translation import gettext_lazy as _
from monerorpc.authproxy import AuthServiceProxy
from pybitid import bitid
def verify_signature(address: str, challenge: str, signature: str) -> bool:
def verify_monero_signature(address: str, challenge: str, signature: str) -> bool:
"""Makes a request to wallet RPC to verify address and signature."""
protocol = settings.DJCL_MONERO_WALLET_RPC_PROTOCOL
host = settings.DJCL_MONERO_WALLET_RPC_HOST
@ -20,6 +24,21 @@ def verify_signature(address: str, challenge: str, signature: str) -> bool:
return result.get("good", False)
def verify_bitcoin_signature(
address: str, challenge: str, signature: str, request: HttpRequest
) -> bool:
"""Verifies if the provided bitcoin signature is valid."""
network = getattr(settings, "DJCL_BITCOIN_NETWORK", None)
if not network:
warnings.warn(_("Please configure the bitcoin network in the settings file"))
is_testnet = True if network == "testnet" else False
callback_uri = request.build_absolute_uri()
return bitid.challenge_valid(
address, signature, challenge, callback_uri, is_testnet
)
def generate_challenge():
"""Generates a new random challenge for the authentication."""
return token_hex(8)

View File

@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
from django.conf import settings
from monero.address import Address
from pybitid.bitid import address_valid
def validate_monero_address(value):
@ -24,3 +25,14 @@ def validate_monero_address(value):
raise ValidationError(_("Invalid address for stagenet"))
elif network == "testnet" and not address.is_testnet():
raise ValidationError(_("Invalid address for testnet"))
def validate_bitcoin_address(value):
network = getattr(settings, "DJCL_BITCOIN_NETWORK", None)
if not network:
raise ValidationError(
_("Please configure the monero network in the settings file")
)
testnet = True if network == "testnet" else False
if not address_valid(value, is_testnet=testnet):
raise ValidationError(_(f"Invalid address for {network}"))

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.views import LoginView
from django.contrib.auth import get_user_model
@ -10,15 +11,16 @@ from django.urls import reverse
from monerorpc.authproxy import JSONRPCException
from .forms import SimpleSignUpForm, SimpleLoginForm
from .utils import verify_signature
from .utils import verify_monero_signature, verify_bitcoin_signature
from .models import Address
class MoneroLoginView(LoginView):
class CryptoLockLoginView(LoginView):
template_name = "django_cryptolock/login.html"
form_class = SimpleLoginForm
class MoneroSignUpView(FormView):
class CryptoLockSignUpView(FormView):
template_name = "django_cryptolock/signup.html"
form_class = SimpleSignUpForm
@ -26,19 +28,20 @@ class MoneroSignUpView(FormView):
return self.form_class(request=self.request, **self.get_form_kwargs())
def form_valid(self, form):
try:
valid_sig = self.verify_signature(form)
except JSONRPCException:
form._errors["__all__"] = ErrorList(
[_("Error connecting to Monero daemon")]
)
return self.form_invalid(form)
username = form.cleaned_data["username"]
address = form.cleaned_data["address"]
challenge = form.cleaned_data["challenge"]
signature = form.cleaned_data["signature"]
try:
valid_sig = verify_signature(address, challenge, signature)
except JSONRPCException:
form._errors["__all__"] = ErrorList([_("Error connecting to daemon")])
return self.form_invalid(form)
if valid_sig:
user = get_user_model().objects.create(username=username)
user.address_set.create(address=address)
user.address_set.create(address=address, network=form.network)
return super().form_valid(form)
else:
form._errors["signature"] = ErrorList([_("Invalid signature")])
@ -46,3 +49,20 @@ class MoneroSignUpView(FormView):
def get_success_url(self):
return settings.LOGIN_REDIRECT_URL
def verify_signature(self, form):
address = form.cleaned_data["address"]
challenge = form.cleaned_data["challenge"]
signature = form.cleaned_data["signature"]
bitcoin = form.network == Address.NETWORK_BITCOIN
monero = form.network == Address.NETWORK_MONERO
valid_sig = False
if bitcoin:
valid_sig = verify_bitcoin_signature(
address, challenge, signature, request=self.request
)
elif monero:
valid_sig = verify_monero_signature(address, challenge, signature)
return valid_sig

View File

@ -116,10 +116,14 @@ LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
# Monero Cryptolock Settings
# Django Cryptolock Settings
# Wallet RPC
AUTHENTICATION_BACKENDS = ["django_cryptolock.backends.MoneroAddressBackend"]
AUTHENTICATION_BACKENDS = [
"django_cryptolock.backends.BitcoinAddressBackend",
"django_cryptolock.backends.MoneroAddressBackend",
]
DJCL_BITCOIN_NETWORK = "mainnet"
DJCL_MONERO_NETWORK = "mainnet"
DJCL_MONERO_WALLET_RPC_PROTOCOL = os.environ.get("MONERO_WALLET_RPC_PROTOCOL", "http")
DJCL_MONERO_WALLET_RPC_HOST = os.environ.get("MONERO_WALLET_RPC_HOST", "localhost:6000")

View File

@ -2,3 +2,4 @@ django>=2.2
django-model-utils>=2.0
monero>=0.6
python-monerorpc>=0.5.5
pybitid>=0.0.4

17
tests/helpers.py Normal file
View File

@ -0,0 +1,17 @@
"""
Set of functions and constants that help testing the existing functionality
"""
def set_monero_settings(settings):
settings.AUTHENTICATION_BACKENDS = [
"django_cryptolock.backends.MoneroAddressBackend",
"django.contrib.auth.backends.ModelBackend",
]
def set_bitcoin_settings(settings):
settings.AUTHENTICATION_BACKENDS = [
"django_cryptolock.backends.BitcoinAddressBackend",
"django.contrib.auth.backends.ModelBackend",
]

View File

@ -29,11 +29,13 @@ else:
AUTHENTICATION_BACKENDS = [
"django_cryptolock.backends.MoneroAddressBackend",
"django_cryptolock.backends.BitcoinAddressBackend",
"django.contrib.auth.backends.ModelBackend",
]
# Test only default settings
DJCL_MONERO_NETWORK = "stagenet"
DJCL_BITCOIN_NETWORK = "mainnet"
DJCL_MONERO_WALLET_RPC_HOST = "localhost:3030"
DJCL_MONERO_WALLET_RPC_USER = "test"

View File

@ -9,12 +9,17 @@ from model_mommy import mommy
from django_cryptolock.models import Address
VALID_ADDRESS = "46fYuhPAdsxMbEeMg97LhSbFPamdiCw7C6b19VEcZSmV6xboWFZuZQ9MTbj1wLszhUExHi63CMtsWjDTrRDqegZiPVebgYq"
User = get_user_model()
from .helpers import set_monero_settings, set_bitcoin_settings
User = get_user_model()
pytestmark = pytest.mark.django_db
DUMMY_CREDS = {"username": "test", "password": "insecure"}
VALID_MONERO_ADDRESS = "46fYuhPAdsxMbEeMg97LhSbFPamdiCw7C6b19VEcZSmV6xboWFZuZQ9MTbj1wLszhUExHi63CMtsWjDTrRDqegZiPVebgYq"
VALID_BITCOIN_ADDRESS = "1N5attoW1FviYGnLmRu9xjaPMKTkWxtUCW"
VALID_BITCOIN_SIG = "H5wI5uqhRCxBpyre2mYkjLxNKPi/TCj9IaHhmfnF8Wn1Pac6gsuYsd2GqTNpy/JFDv3HBSOD75pk2OsGDxE7U4o="
VALID_BITID_URI = "bitid://www.django-cryptolock.test/?x=44d91949c7b2eb20"
EXAMPLE_LOGIN_URL = "https://www.django-cryptolock.test/"
@pytest.fixture
@ -22,29 +27,33 @@ def existing_user():
return User.objects.create_user(**DUMMY_CREDS)
def test_monero_backend_receives_insuficient_data(existing_user):
def test_monero_backend_receives_insuficient_data(settings, existing_user):
set_monero_settings(settings)
user = authenticate(MagicMock(), username="test")
assert user is None
def test_monero_backend_lets_the_next_backend_to_be_used(existing_user):
def test_monero_backend_lets_the_next_backend_to_be_used(settings, existing_user):
set_monero_settings(settings)
user = authenticate(MagicMock(), **DUMMY_CREDS)
assert user is not None
def test_monero_backend_does_not_find_address(existing_user):
def test_monero_backend_does_not_find_address(settings, existing_user):
set_monero_settings(settings)
user = authenticate(
MagicMock(), address=VALID_ADDRESS, challeng="1", signature="somesig"
MagicMock(), address=VALID_MONERO_ADDRESS, challeng="1", signature="somesig"
)
assert user is None
def test_monero_backend_cannot_connect_to_RPC(existing_user):
mommy.make(Address, address=VALID_ADDRESS, user=existing_user)
def test_monero_backend_cannot_connect_to_RPC(settings, existing_user):
set_monero_settings(settings)
mommy.make(Address, address=VALID_MONERO_ADDRESS, user=existing_user)
user = authenticate(
MagicMock(),
address=VALID_ADDRESS,
address=VALID_MONERO_ADDRESS,
challenge="1",
signature="invalid sig",
**DUMMY_CREDS
@ -53,25 +62,101 @@ def test_monero_backend_cannot_connect_to_RPC(existing_user):
assert user is None
def test_monero_backend_invalid_signature(existing_user):
mommy.make(Address, address=VALID_ADDRESS, user=existing_user)
def test_monero_backend_invalid_signature(settings, existing_user):
set_monero_settings(settings)
mommy.make(Address, address=VALID_MONERO_ADDRESS, user=existing_user)
with patch("django_cryptolock.backends.verify_signature") as verify_mock:
with patch("django_cryptolock.backends.verify_monero_signature") as verify_mock:
verify_mock.return_value = False
user = authenticate(
MagicMock(), address=VALID_ADDRESS, challenge="1", signature="invalid sig"
MagicMock(),
address=VALID_MONERO_ADDRESS,
challenge="1",
signature="invalid sig",
)
assert user is None
def test_monero_backed_valid_signature(existing_user):
mommy.make(Address, address=VALID_ADDRESS, user=existing_user)
def test_monero_backend_valid_signature(settings, existing_user):
set_monero_settings(settings)
mommy.make(Address, address=VALID_MONERO_ADDRESS, user=existing_user)
with patch("django_cryptolock.backends.verify_signature") as verify_mock:
with patch("django_cryptolock.backends.verify_monero_signature") as verify_mock:
verify_mock.return_value = True
user = authenticate(
MagicMock(), address=VALID_ADDRESS, challenge="1", signature="valid sig"
MagicMock(),
address=VALID_MONERO_ADDRESS,
challenge="1",
signature="valid sig",
)
assert user == existing_user
def test_bitcoin_backend_receives_insuficient_data(settings, existing_user):
set_bitcoin_settings(settings)
user = authenticate(MagicMock(), username="test")
assert user is None
def test_bitcoin_backend_lets_the_next_backend_to_be_used(settings, existing_user):
set_bitcoin_settings(settings)
user = authenticate(MagicMock(), **DUMMY_CREDS)
assert user is not None
def test_bitcoin_backend_does_not_find_address(settings, existing_user):
set_bitcoin_settings(settings)
user = authenticate(
MagicMock(),
address=VALID_BITCOIN_ADDRESS,
bitid_uri="bitid://something",
signature="somesig",
)
assert user is None
def test_bitcoin_backend_invalid_signature(settings, existing_user):
set_bitcoin_settings(settings)
mommy.make(
Address,
address=VALID_BITCOIN_ADDRESS,
network=Address.NETWORK_BITCOIN,
user=existing_user,
)
mock = MagicMock()
mock.build_absolute_uri.return_value = EXAMPLE_LOGIN_URL
user = authenticate(
mock,
address=VALID_BITCOIN_ADDRESS,
bitid_uri=VALID_BITID_URI,
signature="invalid sig",
)
assert user is None
def test_bitcoin_backend_valid_signature(settings, existing_user):
set_bitcoin_settings(settings)
set_bitcoin_settings(settings)
mommy.make(
Address,
address=VALID_BITCOIN_ADDRESS,
network=Address.NETWORK_BITCOIN,
user=existing_user,
)
mock = MagicMock()
mock.build_absolute_uri.return_value = EXAMPLE_LOGIN_URL
user = authenticate(
mock,
address=VALID_BITCOIN_ADDRESS,
challenge=VALID_BITID_URI,
signature=VALID_BITCOIN_SIG,
)
assert user == existing_user

View File

@ -8,9 +8,12 @@ from model_mommy import mommy
from django_cryptolock.forms import SimpleLoginForm, SimpleSignUpForm
from django_cryptolock.models import Address
from .helpers import set_monero_settings, set_bitcoin_settings
pytestmark = pytest.mark.django_db
VALID_ADDRESS = "46fYuhPAdsxMbEeMg97LhSbFPamdiCw7C6b19VEcZSmV6xboWFZuZQ9MTbj1wLszhUExHi63CMtsWjDTrRDqegZiPVebgYq"
VALID_MONERO_ADDRESS = "46fYuhPAdsxMbEeMg97LhSbFPamdiCw7C6b19VEcZSmV6xboWFZuZQ9MTbj1wLszhUExHi63CMtsWjDTrRDqegZiPVebgYq"
VALID_BITCOIN_ADDRESS = "1N5attoW1FviYGnLmRu9xjaPMKTkWxtUCW"
User = get_user_model()
@ -19,9 +22,11 @@ def test_simpleloginform_generates_new_challenge():
initial = {}
request.session.__setitem__.side_effect = initial.__setitem__
request.session.__getitem__.side_effect = initial.__getitem__
request.build_absolute_uri.return_value = "http://something/"
form = SimpleLoginForm(request=request)
assert form.initial.get("challenge")
assert initial["current_challenge"] == form.initial.get("challenge")
assert form.initial.get("challenge").startswith("bitid://something")
def test_simpleloginform_generates_no_new_challenge():
@ -29,6 +34,7 @@ def test_simpleloginform_generates_no_new_challenge():
initial = {}
request.session.__setitem__.side_effect = initial.__setitem__
request.session.__getitem__.side_effect = initial.__getitem__
request.build_absolute_uri.return_value = "http://something/"
form = SimpleLoginForm(request=request, data={"address": ""})
assert not form.initial.get("challenge")
assert not initial.get("current_challenge")
@ -38,10 +44,11 @@ def test_simpleloginform_generates_no_new_challenge():
def test_simpleloginform_valid_data(settings):
settings.DJCL_MONERO_NETWORK = "mainnet"
request = MagicMock()
request.build_absolute_uri.return_value = "http://something/"
form = SimpleLoginForm(
request=request,
data={
"address": VALID_ADDRESS,
"address": VALID_MONERO_ADDRESS,
"challenge": "12345678",
"signature": "some valid signature",
},
@ -52,21 +59,24 @@ def test_simpleloginform_valid_data(settings):
assert form.is_valid()
def test_simplesignupform_generaes_new_challenge():
def test_simplesignupform_generates_new_challenge():
request = MagicMock()
initial = {}
request.session.__setitem__.side_effect = initial.__setitem__
request.session.__getitem__.side_effect = initial.__getitem__
request.build_absolute_uri.return_value = "http://something/"
form = SimpleSignUpForm(request=request)
assert form.initial.get("challenge")
assert initial["current_challenge"] == form.initial.get("challenge")
assert form.initial.get("challenge").startswith("bitid://something")
def test_simplesignupform_generaes_no_new_challenge():
def test_simplesignupform_generates_no_new_challenge():
request = MagicMock()
initial = {}
request.session.__setitem__.side_effect = initial.__setitem__
request.session.__getitem__.side_effect = initial.__getitem__
request.build_absolute_uri.return_value = "http://something/"
form = SimpleSignUpForm(request=request, data={"address": ""})
assert not form.initial.get("challenge")
assert not initial.get("current_challenge")
@ -74,16 +84,68 @@ def test_simplesignupform_generaes_no_new_challenge():
def test_validate_address_unique(settings):
settings.DJCL_MONERO_NETWORK = "mainnet"
mommy.make(Address, address=VALID_ADDRESS)
mommy.make(Address, address=VALID_MONERO_ADDRESS)
request = MagicMock()
request.build_absolute_uri.return_value = "http://something/"
form = SimpleSignUpForm(
request=request,
data={
"username": "foo",
"address": VALID_ADDRESS,
"address": VALID_MONERO_ADDRESS,
"challenge": "12345678",
"signature": "some valid signature",
},
)
assert not form.is_valid()
assert "This address already exists" in form.errors["address"]
def test_simplesignupform_validate_bitcoin_addr(settings):
set_bitcoin_settings(settings)
request = MagicMock()
request.build_absolute_uri.return_value = "http://something/"
request.session.get.return_value = "12345678"
form = SimpleSignUpForm(
request=request,
data={
"username": "foo",
"address": VALID_BITCOIN_ADDRESS,
"challenge": "12345678",
"signature": "some valid signature",
},
)
assert form.is_valid()
def test_simplesignupform_valid_monero_addr(settings):
set_monero_settings(settings)
settings.DJCL_MONERO_NETWORK = "mainnet"
request = MagicMock()
request.build_absolute_uri.return_value = "http://something/"
request.session.get.return_value = "12345678"
form = SimpleSignUpForm(
request=request,
data={
"username": "foo",
"address": VALID_MONERO_ADDRESS,
"challenge": "12345678",
"signature": "some valid signature",
},
)
assert form.is_valid()
def test_simplesignupform_validate_invalid_addr():
request = MagicMock()
request.build_absolute_uri.return_value = "http://something/"
form = SimpleSignUpForm(
request=request,
data={
"username": "foo",
"address": "bad addr",
"challenge": "12345678",
"signature": "some valid signature",
},
)
assert not form.is_valid()
assert "Invalid address" in form.errors["address"]

View File

@ -19,30 +19,51 @@ VALID_MONERO_MAINNET_ADDR = "45D8b4XiUdz86FwztAJHVeLnQqGHQUqiHSwZe6rXFHSoXw522dP
VALID_MONERO_STAGENET_ADDR = "55LTR8KniP4LQGJSPtbYDacR7dz8RBFnsfAKMaMuwUNYX6aQbBcovzDPyrQF9KXF9tVU6Xk3K8no1BywnJX6GvZX8yJsXvt"
VALID_MONERO_TESTNET_ADDR = "9vmn8Vyxh6JEVmPr4qTcj3ND3FywDpMXH2fVLLEARyKCJTc3jWjxeWcbRNcaa57Bj36cARBSfWnfS89oFVKBBvGTAegdRxG"
VALID_BITCOIN_TESTNET_ADDR = "n47QBape2PcisN2mkHR2YnhqoBr56iPhJh"
VALID_BITCOIN_MAINNET_ADDR = "1AUeWMGD9hDYtAhZGZLmDjEzKSrPow4zNt"
pytestmark = pytest.mark.django_db
def test_valid_mainnet_address(settings):
def test_valid_monero_mainnet_address(settings):
settings.DJCL_MONERO_NETWORK = "mainnet"
addr = mommy.make(Address, address=VALID_MONERO_MAINNET_ADDR)
addr.full_clean()
def test_valid_stagenet_addr(settings):
def test_valid_monero_stagenet_addr(settings):
settings.DJCL_MONERO_NETWORK = "stagenet"
addr = mommy.make(Address, address=VALID_MONERO_STAGENET_ADDR)
addr.full_clean()
def test_valid_testnet_addr(settings):
def test_valid_monero_testnet_addr(settings):
settings.DJCL_MONERO_NETWORK = "testnet"
addr = mommy.make(Address, address=VALID_MONERO_TESTNET_ADDR)
addr.full_clean()
def test_valid_bitcoin_mainnet_address(settings):
settings.DJCL_BITCOIN_NETWORK = "mainnet"
addr = mommy.make(
Address, address=VALID_BITCOIN_MAINNET_ADDR, network=Address.NETWORK_BITCOIN
)
addr.full_clean()
def test_valid_bitcoin_testnet_address(settings):
settings.DJCL_BITCOIN_NETWORK = "testnet"
addr = mommy.make(
Address, address=VALID_BITCOIN_TESTNET_ADDR, network=Address.NETWORK_BITCOIN
)
addr.full_clean()
def test_invalid_address():
bad_addr = "Verywrongaddress"
addr = mommy.make(Address, address=bad_addr)
@ -51,23 +72,38 @@ def test_invalid_address():
addr.full_clean()
assert (
"{} is not a valid address".format(bad_addr)
in error.value.message_dict["address"]
"Invalid address for the given network" in error.value.message_dict["__all__"]
)
def test_wrong_network_address(settings):
def test_wrong_monero_network_address(settings):
settings.DJCL_MONERO_NETWORK = "stagenet"
addr = mommy.make(Address, address=VALID_MONERO_MAINNET_ADDR)
with pytest.raises(ValidationError) as error:
addr.full_clean()
assert "Invalid address for stagenet" in error.value.message_dict["address"]
assert (
"Invalid address for the given network" in error.value.message_dict["__all__"]
)
def test_wrong_bitcoin_network_address(settings):
settings.DJCL_BITCOIN_NETWORK = "testnet"
addr = mommy.make(
Address, address=VALID_BITCOIN_MAINNET_ADDR, network=Address.NETWORK_BITCOIN
)
with pytest.raises(ValidationError) as error:
addr.full_clean()
assert (
"Invalid address for the given network" in error.value.message_dict["__all__"]
)
def test_address_is_unique():
addr = mommy.make(Address, address=VALID_MONERO_MAINNET_ADDR)
mommy.make(Address, address=VALID_MONERO_MAINNET_ADDR)
with pytest.raises(IntegrityError):
mommy.make(Address, address=VALID_MONERO_MAINNET_ADDR)