Completing functional conversions
This commit is contained in:
parent
3cfd9840e8
commit
edb16382f3
29 changed files with 1798 additions and 28 deletions
18
CHANGELOG
18
CHANGELOG
|
@ -1,3 +1,21 @@
|
|||
Mon, 02 Sep 2019 04:56:45 -0500
|
||||
Keaton <kii-chan@tutanota.com>
|
||||
Completing functional conversions
|
||||
|
||||
Not much is verified to work yet. All tests pass, however functionality
|
||||
is not guaranteed. This update is being pushed to the dev git in order
|
||||
for an easier setup of a test server with SSL.
|
||||
|
||||
Things changed: No idea, just squashed bugs that were causing tests to
|
||||
fail.
|
||||
|
||||
Future development note: The original package supports insecure "qrl://"
|
||||
schema, and as far as I can tell, that's not supported. I will probably
|
||||
post on the SQRL forums about it, and remove the functionality completely
|
||||
if the consensus is that it is not legitimate.
|
||||
|
||||
--------------------
|
||||
|
||||
Mon, 02 Sep 2019 02:03:23 -0500
|
||||
Keaton <kii-chan@tutanota.com>
|
||||
Still building initial conversions
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
__author__ = 'Miroslav Shubernetskiy | Keaton Brown'
|
||||
__email__ = 'miroslav@miki725.com | kii-chan@tutanota.com'
|
||||
__version__ = '0.1.0'
|
|
@ -1,5 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SqrlConfig(AppConfig):
|
||||
name = 'sqrl'
|
44
sqrl/backends.py
Normal file
44
sqrl/backends.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
from .models import SQRLIdentity
|
||||
|
||||
|
||||
class SQRLModelBackend(ModelBackend):
|
||||
"""
|
||||
Custom SQRL Authentication backend which honors ``only_sqrl`` when enabled.
|
||||
|
||||
SQRL, by its spec, allows users to send ``only_sqrl`` flag to the server
|
||||
which indicates to the server that it should only use SQRL
|
||||
for authentication and disable all other methods of authentication.
|
||||
This custom authentication backend implements that requirement.
|
||||
It honors the ``only_sqrl`` spec and does not allow to authenticate
|
||||
a user when following conditions are all ``True``:
|
||||
|
||||
* user successfully validated credentials using traditional auth method
|
||||
* user has SQRL identity associated with their account
|
||||
* :attr:`.models.SQRLIdentity.in_only_sqrl` is ``True``
|
||||
"""
|
||||
|
||||
def authenticate(self, *args, **kwargs):
|
||||
"""
|
||||
Same as Django's ``ModelBackend.authenticate`` except
|
||||
this method honors ``only_sqrl`` SQRL spec.
|
||||
"""
|
||||
user = super(SQRLModelBackend, self).authenticate(*args, **kwargs)
|
||||
|
||||
if user is None:
|
||||
return
|
||||
|
||||
try:
|
||||
sqrl_identity = user.sqrl_identity
|
||||
except SQRLIdentity.DoesNotExist:
|
||||
return user
|
||||
else:
|
||||
if sqrl_identity.is_only_sqrl and sqrl_identity.is_enabled:
|
||||
return
|
||||
|
||||
return user
|
||||
|
||||
|
||||
SQRL_MODEL_BACKEND = '{}.{}'.format(SQRLModelBackend.__module__, SQRLModelBackend.__name__)
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from collections import OrderedDict
|
||||
from os import urandom
|
||||
import os
|
||||
|
||||
import ed25519
|
||||
from django.utils.crypto import constant_time_compare, salted_hmac
|
||||
|
@ -9,6 +10,8 @@ from .utils import Base64, Encoder
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
class HMAC(object):
|
||||
"""
|
||||
Utility class for generating and verifying HMAC signatures.
|
||||
|
@ -131,4 +134,4 @@ def generate_randomness(size=32):
|
|||
str
|
||||
:meth:`.Base64.encode` encoded random sample
|
||||
"""
|
||||
return Base64.encode(urandom(size))
|
||||
return Base64.encode(bytearray(os.urandom(size)))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.http import QueryDict
|
||||
|
||||
from .crypto import generate_randomness
|
||||
|
@ -139,7 +139,7 @@ class SQRLInitialization(object):
|
|||
"""
|
||||
return (
|
||||
'{scheme}://{host}{url}'
|
||||
''.format(scheme='sqrl' if self.request.is_secure() else 'qrl',
|
||||
''.format(scheme='sqrl', if self.request.is_secure() else 'qrl',
|
||||
host=self.request.get_host(),
|
||||
url=self.url)
|
||||
)
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
max-width: 300px;
|
||||
border-radius: 5%;
|
||||
}
|
||||
#sqrl-id img {
|
||||
#sqrl-qr img {
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
padding-top: 3px;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
(function() {
|
||||
var get_next_url = function() {
|
||||
var input = document.querySelectorAll('input[name="next"]');
|
||||
|
@ -59,7 +57,7 @@
|
|||
// The original can be found here:
|
||||
// https://davidshimjs.github.io/qrcodejs/
|
||||
|
||||
var QRCode;
|
||||
var QRCode;
|
||||
! function() {
|
||||
function a(a) {
|
||||
this.mode = c.MODE_8BIT_BYTE, this.data = a, this.parsedData = [];
|
||||
|
|
|
@ -82,10 +82,10 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
{% sqrl_status_url_script_tag session_sqrl %}
|
||||
<script type="text/javascript">
|
||||
SQRL_CHECK_URL="{% sqrl_status_url_script_tag session_sqrl %}"
|
||||
document.getElementById('id_username').focus()
|
||||
</script>
|
||||
<script src="{% static 'sqrl/sqrl.js' %}"></script>
|
||||
<script type="application/javascript" src="{% static 'sqrl/sqrl.js' %}"></script>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
46
sqrl/templates/base.html
Normal file
46
sqrl/templates/base.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
{% load static %}
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>{% block title %}SQRL Test{% endblock %}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header>
|
||||
{% block header %}
|
||||
<div>SQRL Test Server</div>
|
||||
<nav>
|
||||
{% if user.is_authenticated %}
|
||||
Logged in as {{ user.username }}
|
||||
{% endif %}
|
||||
<ul>
|
||||
{% if user.is_authenticated %}
|
||||
<li>
|
||||
<a href="{% url 'logout' %}">Log out</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'sqrl:manage' %}">Manage SQRL</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a href="{% url 'sqrl:login' %}">Log In</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
</header>
|
||||
|
||||
<div id="content">
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -8,7 +8,7 @@
|
|||
max-width: 300px;
|
||||
border-radius: 5%;
|
||||
}
|
||||
#sqrl-id img {
|
||||
#sqrl-qr img {
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
padding-top: 3px;
|
||||
|
@ -37,5 +37,5 @@
|
|||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% sqrl_status_url_script_tag session_sqrl %}
|
||||
<script src="{% static sqrl/sqrl.js %}"></script>
|
||||
<script>SQRL_CHECK_URL="{% sqrl_status_url_script_tag session_sqrl %}"</script>
|
||||
<script type="application/javascript" src="{% static 'sqrl/sqrl.js' %}"></script>
|
||||
|
|
|
@ -7,9 +7,10 @@ from ..sqrl import SQRLInitialization
|
|||
|
||||
|
||||
register = template.Library()
|
||||
print(register)
|
||||
|
||||
|
||||
@register.assignment_tag(takes_context=True)
|
||||
@register.simple_tag(takes_context=True)
|
||||
def sqrl(context):
|
||||
return SQRLInitialization(context['request'])
|
||||
|
||||
|
@ -41,4 +42,4 @@ def sqrl_login_dropin(session_sqrl, method="login"):
|
|||
@register.simple_tag
|
||||
def sqrl_status_url_script_tag(sqrl):
|
||||
url = reverse('sqrl:status', kwargs={'transaction': sqrl.nut.transaction_nonce})
|
||||
return '<script>SQRL_CHECK_URL="{url}"</script>'.format(url=url)
|
||||
return url
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
1
sqrl/tests/__init__.py
Normal file
1
sqrl/tests/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
58
sqrl/tests/test_backends.py
Normal file
58
sqrl/tests/test_backends.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
from ..backends import SQRLModelBackend
|
||||
from ..models import SQRLIdentity
|
||||
|
||||
|
||||
class TestSQRLModelBackend(unittest.TestCase):
|
||||
@mock.patch.object(ModelBackend, 'authenticate')
|
||||
def test_authenticate_no_user(self, mock_authenticate):
|
||||
mock_authenticate.return_value = None
|
||||
|
||||
self.assertIsNone(SQRLModelBackend().authenticate(
|
||||
username='user',
|
||||
password='password'
|
||||
))
|
||||
|
||||
@mock.patch.object(ModelBackend, 'authenticate')
|
||||
def test_authenticate_no_sqrl_identity(self, mock_authenticate):
|
||||
class UserMock(mock.MagicMock):
|
||||
@property
|
||||
def sqrl_identity(self):
|
||||
raise SQRLIdentity.DoesNotExist
|
||||
|
||||
user = UserMock()
|
||||
mock_authenticate.return_value = user
|
||||
|
||||
self.assertEqual(SQRLModelBackend().authenticate(
|
||||
username='user',
|
||||
password='password'
|
||||
), user)
|
||||
|
||||
@mock.patch.object(ModelBackend, 'authenticate')
|
||||
def test_authenticate_disabled(self, mock_authenticate):
|
||||
user = mock.MagicMock()
|
||||
user.sqrl_identity.is_only_sqrl = True
|
||||
user.sqrl_identity.is_enabled = True
|
||||
mock_authenticate.return_value = user
|
||||
|
||||
self.assertIsNone(SQRLModelBackend().authenticate(
|
||||
username='user',
|
||||
password='password'
|
||||
))
|
||||
|
||||
@mock.patch.object(ModelBackend, 'authenticate')
|
||||
def test_authenticate_enabled(self, mock_authenticate):
|
||||
user = mock.MagicMock()
|
||||
user.sqrl_identity.is_only_sqrl = False
|
||||
user.sqrl_identity.is_enabled = True
|
||||
mock_authenticate.return_value = user
|
||||
|
||||
self.assertEqual(SQRLModelBackend().authenticate(
|
||||
username='user',
|
||||
password='password'
|
||||
), user)
|
186
sqrl/tests/test_crypto.py
Normal file
186
sqrl/tests/test_crypto.py
Normal file
|
@ -0,0 +1,186 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
import ed25519
|
||||
import mock
|
||||
|
||||
from ..crypto import HMAC, Ed25519, generate_randomness
|
||||
from ..models import SQRLNut
|
||||
from ..utils import Base64
|
||||
|
||||
|
||||
TESTING_MODULE = 'sqrl.crypto'
|
||||
|
||||
|
||||
class TestHMAC(unittest.TestCase):
|
||||
def setUp(self):
|
||||
super(TestHMAC, self).setUp()
|
||||
self.nut = SQRLNut(session_key='0123456789')
|
||||
self.data = OrderedDict([
|
||||
('ver', '1'),
|
||||
('tif', 0),
|
||||
('mac', 'value'),
|
||||
('qry', 'foo?nut=bar'),
|
||||
])
|
||||
self.hmac = HMAC(self.nut, self.data)
|
||||
|
||||
def test_init(self):
|
||||
hmac = HMAC(mock.sentinel.nut, mock.sentinel.data)
|
||||
|
||||
self.assertEqual(hmac.nut, mock.sentinel.nut)
|
||||
self.assertEqual(hmac.data, mock.sentinel.data)
|
||||
|
||||
def test_sign_data(self):
|
||||
temp = str(settings.SECRET_KEY)
|
||||
settings.SECRET_KEY = "foo"
|
||||
signature = self.hmac.sign_data()
|
||||
|
||||
self.assertIsInstance(signature, bytes)
|
||||
self.assertEqual(len(signature), 20)
|
||||
self.assertEqual(
|
||||
signature,
|
||||
b'R\xfc\xb2\xbd\x12\x85\xae\xb0>\xdd\xed\x16P\xc2\x82\xae\x06\x0c\xc5\xd3'
|
||||
)
|
||||
settings.SECRET_KEY = str(temp)
|
||||
|
||||
@mock.patch(TESTING_MODULE + '.salted_hmac')
|
||||
def test_sign_data_mock(self, mock_salted_hmac):
|
||||
signature = self.hmac.sign_data()
|
||||
|
||||
self.assertEqual(
|
||||
signature,
|
||||
mock_salted_hmac.return_value.digest.return_value
|
||||
)
|
||||
mock_salted_hmac.assert_called_once_with(
|
||||
self.nut.session_key,
|
||||
Base64.encode(b'ver=1\r\n'
|
||||
b'tif=0\r\n'
|
||||
b'qry=foo?nut=bar\r\n')
|
||||
)
|
||||
|
||||
def test_sign_data_not_dict(self):
|
||||
with self.assertRaises(AssertionError):
|
||||
HMAC(mock.sentinel.nut, mock.sentinel.data).sign_data()
|
||||
|
||||
@mock.patch.object(HMAC, 'sign_data')
|
||||
def test_is_signature_valid(self, mock_sign_data):
|
||||
mock_sign_data.return_value = 'foo_signature'
|
||||
|
||||
self.assertTrue(self.hmac.is_signature_valid('foo_signature'))
|
||||
self.assertFalse(self.hmac.is_signature_valid('foo-signature'))
|
||||
|
||||
def test_validation_loop(self):
|
||||
signature = self.hmac.sign_data()
|
||||
|
||||
self.assertTrue(self.hmac.is_signature_valid(signature))
|
||||
self.assertFalse(self.hmac.is_signature_valid(b'a' + signature[:-1]))
|
||||
|
||||
|
||||
class TestEd25519(unittest.TestCase):
|
||||
def setUp(self):
|
||||
super(TestEd25519, self).setUp()
|
||||
self.signing_key = (b'\xbbH\xdfx\xed\xc5\xdbR\x94\xe4\xff\xa6~5\xbb\xbd\xf2\x16&'
|
||||
b'\xfc\x89\x8a\xc8\\\\\xeb\xea\x91Db~Hm+b\x88\xf2\x10\xfb:H'
|
||||
b'\xe4\xfb0\x00\r\xe7n|\xa64\x05m@\xc8\xef"\x07k{O\xf0\xff%')
|
||||
self.verifying_key = (b'm+b\x88\xf2\x10\xfb:H\xe4\xfb0\x00\r\xe7n|\xa64\x05m@'
|
||||
b'\xc8\xef"\x07k{O\xf0\xff%')
|
||||
self.data = b'data'
|
||||
self.sig = Ed25519(self.verifying_key, self.signing_key, self.data)
|
||||
|
||||
def test_init(self):
|
||||
sig = Ed25519(mock.sentinel.pub_key,
|
||||
mock.sentinel.priv_key,
|
||||
mock.sentinel.msg)
|
||||
|
||||
self.assertEqual(sig.public_key, mock.sentinel.pub_key)
|
||||
self.assertEqual(sig.private_key, mock.sentinel.priv_key)
|
||||
self.assertEqual(sig.msg, mock.sentinel.msg)
|
||||
|
||||
def test_sign_data(self):
|
||||
signature = self.sig.sign_data()
|
||||
|
||||
self.assertIsInstance(signature, bytes)
|
||||
self.assertEqual(len(signature), 64)
|
||||
self.assertEqual(
|
||||
signature,
|
||||
b'\xac\xe0\x81\xc4\xd5\x7f\xd4\xe3\xc1\x03>\x0f\x90\xb5\x9eG<\xe0\xd41'
|
||||
b'\x1cZ\xd7\x15F\xba\xdeS/\xfa\xbbL\x9bh\x8dn;\xcfP\xb1\x16\x14&d\xde'
|
||||
b'\x97\x145\x90N[\xb9\xfc\x8e\x8a\x9e\xd2=\xad\x84\xcd\xf1\x93\x06'
|
||||
)
|
||||
|
||||
@mock.patch('ed25519.SigningKey')
|
||||
def test_sign_data_mock(self, mock_signing_key):
|
||||
signature = self.sig.sign_data()
|
||||
|
||||
self.assertEqual(signature, mock_signing_key.return_value.sign.return_value)
|
||||
mock_signing_key.assert_called_once_with(self.sig.private_key)
|
||||
mock_signing_key.return_value.sign.assert_called_once_with(self.sig.msg)
|
||||
|
||||
def test_is_signature_valid(self):
|
||||
signature = self.sig.sign_data()
|
||||
|
||||
self.assertTrue(self.sig.is_signature_valid(signature))
|
||||
self.assertFalse(self.sig.is_signature_valid(b'a' + signature[:-1]))
|
||||
|
||||
@mock.patch('ed25519.VerifyingKey')
|
||||
def test_is_signature_mock(self, mock_verifying_key):
|
||||
is_valid = self.sig.is_signature_valid(mock.sentinel.signature)
|
||||
|
||||
self.assertTrue(is_valid)
|
||||
mock_verifying_key.assert_called_once_with(self.sig.public_key)
|
||||
mock_verifying_key.return_value.verify.assert_called_once_with(
|
||||
mock.sentinel.signature, self.data
|
||||
)
|
||||
|
||||
@mock.patch('ed25519.VerifyingKey')
|
||||
def test_is_signature_mock_assertion_error(self, mock_verifying_key):
|
||||
mock_verifying_key.return_value.verify.side_effect = AssertionError
|
||||
|
||||
is_valid = self.sig.is_signature_valid(mock.sentinel.signature)
|
||||
|
||||
self.assertFalse(is_valid)
|
||||
mock_verifying_key.assert_called_once_with(self.sig.public_key)
|
||||
mock_verifying_key.return_value.verify.assert_called_once_with(
|
||||
mock.sentinel.signature, self.data
|
||||
)
|
||||
|
||||
@mock.patch('ed25519.VerifyingKey')
|
||||
def test_is_signature_mock_bas_signature_error(self, mock_verifying_key):
|
||||
mock_verifying_key.return_value.verify.side_effect = ed25519.BadSignatureError
|
||||
|
||||
is_valid = self.sig.is_signature_valid(mock.sentinel.signature)
|
||||
|
||||
self.assertFalse(is_valid)
|
||||
mock_verifying_key.assert_called_once_with(self.sig.public_key)
|
||||
mock_verifying_key.return_value.verify.assert_called_once_with(
|
||||
mock.sentinel.signature, self.data
|
||||
)
|
||||
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
@mock.patch(TESTING_MODULE + '.bytearray', create=True)
|
||||
@mock.patch.object(Base64, 'encode')
|
||||
@mock.patch.object(os, 'urandom')
|
||||
def test_generate_randomness_mock(self, mock_urandom, mock_encode, mock_bytearray):
|
||||
_mock_bytearray = mock.MagicMock()
|
||||
|
||||
def _bytearray(a):
|
||||
list(a)
|
||||
return _mock_bytearray(a)
|
||||
|
||||
mock_bytearray.side_effect = _bytearray
|
||||
|
||||
randomness = generate_randomness()
|
||||
|
||||
self.assertEqual(randomness, mock_encode.return_value)
|
||||
self.assertEqual(mock_urandom.call_count, 1)
|
||||
mock_urandom.assert_called_with(32)
|
||||
mock_encode.assert_called_once_with(_mock_bytearray.return_value)
|
||||
|
||||
def test_generate_randomness(self):
|
||||
randomness = generate_randomness()
|
||||
|
||||
self.assertIsInstance(randomness, str)
|
56
sqrl/tests/test_exceptions.py
Normal file
56
sqrl/tests/test_exceptions.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from ..exceptions import TIF, TIFException
|
||||
|
||||
|
||||
TESTING_MODULE = 'sqrl.exceptions'
|
||||
|
||||
|
||||
class TestTIF(unittest.TestCase):
|
||||
def test_as_hex_string(self):
|
||||
self.assertEqual(TIF(0x1).as_hex_string(), '1')
|
||||
self.assertEqual(TIF(0x4).as_hex_string(), '4')
|
||||
self.assertEqual(TIF(0x88).as_hex_string(), '88')
|
||||
self.assertEqual(TIF(0x84).as_hex_string(), '84')
|
||||
|
||||
def test_breakdown(self):
|
||||
self.assertDictEqual(TIF(0x34).breakdown(), {
|
||||
'id_match': False,
|
||||
'previous_id_match': False,
|
||||
'ip_match': True,
|
||||
'sqrl_disabled': False,
|
||||
'not_supported': True,
|
||||
'transient_failure': True,
|
||||
'command_failed': False,
|
||||
'client_failure': False,
|
||||
'bad_id_association': False,
|
||||
})
|
||||
|
||||
def test_update(self):
|
||||
tif = TIF(0x3).update(TIF(0x40))
|
||||
|
||||
self.assertIsInstance(tif, TIF)
|
||||
self.assertEqual(tif, 0x43)
|
||||
|
||||
def test_properties(self):
|
||||
self.assertTrue(TIF(TIF.ID_MATCH).is_id_match)
|
||||
self.assertTrue(TIF(TIF.PREVIOUS_ID_MATCH).is_previous_id_match)
|
||||
self.assertTrue(TIF(TIF.IP_MATCH).is_ip_match)
|
||||
self.assertTrue(TIF(TIF.SQRL_DISABLED).is_sqrl_disabled)
|
||||
self.assertTrue(TIF(TIF.NOT_SUPPORTED).is_not_supported)
|
||||
self.assertTrue(TIF(TIF.TRANSIENT_FAILURE).is_transient_failure)
|
||||
self.assertTrue(TIF(TIF.COMMAND_FAILED).is_command_failed)
|
||||
self.assertTrue(TIF(TIF.CLIENT_FAILURE).is_client_failure)
|
||||
self.assertTrue(TIF(TIF.BAD_ID_ASSOCIATION).is_bad_id_association)
|
||||
|
||||
|
||||
class TestTIFException(unittest.TestCase):
|
||||
@mock.patch(TESTING_MODULE + '.TIF')
|
||||
def test_init(self, mock_tif):
|
||||
e = TIFException(mock.sentinel.tif)
|
||||
|
||||
self.assertEqual(e.tif, mock_tif.return_value)
|
||||
mock_tif.assert_called_once_with(mock.sentinel.tif)
|
196
sqrl/tests/test_fields.py
Normal file
196
sqrl/tests/test_fields.py
Normal file
|
@ -0,0 +1,196 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
|
||||
import mock
|
||||
from django import forms
|
||||
from django.urls import Resolver404
|
||||
|
||||
from ..fields import (
|
||||
Base64CharField,
|
||||
Base64ConditionalPairsField,
|
||||
Base64Field,
|
||||
Base64PairsField,
|
||||
ExtractedNextUrlField,
|
||||
NextUrlField,
|
||||
SQRLURLField,
|
||||
SQRLURLValidator,
|
||||
TildeMultipleValuesField,
|
||||
TildeMultipleValuesFieldChoiceField,
|
||||
)
|
||||
from ..utils import Base64
|
||||
|
||||
|
||||
TESTING_MODULE = 'sqrl.fields'
|
||||
|
||||
|
||||
class TestNextUrlField(unittest.TestCase):
|
||||
def test_to_python_empty(self):
|
||||
self.assertEqual(NextUrlField().to_python(None), '')
|
||||
|
||||
@mock.patch(TESTING_MODULE + '.resolve')
|
||||
def test_to_python_valid(self, mock_resolve):
|
||||
value = 'http://example.com/path/here/?querystring=here'
|
||||
self.assertEqual(NextUrlField().to_python(value), '/path/here/')
|
||||
|
||||
@mock.patch(TESTING_MODULE + '.resolve')
|
||||
def test_to_python_invalid(self, mock_resolve):
|
||||
mock_resolve.side_effect = Resolver404
|
||||
value = 'http://example.com/path/here/?querystring=here'
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
NextUrlField().to_python(value)
|
||||
|
||||
|
||||
class TestExtractedNextUrlField(unittest.TestCase):
|
||||
def test_to_python_empty(self):
|
||||
self.assertEqual(ExtractedNextUrlField().to_python(None), '')
|
||||
|
||||
def test_to_python_next_not_present(self):
|
||||
value = 'http://example.com/path/here/?querystring=here'
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
ExtractedNextUrlField().to_python(value)
|
||||
|
||||
@mock.patch(TESTING_MODULE + '.resolve')
|
||||
def test_to_python(self, mock_resolve):
|
||||
value = 'http://example.com/path/here/?next=/next/here/'
|
||||
|
||||
self.assertEqual(
|
||||
ExtractedNextUrlField().to_python(value),
|
||||
'/next/here/'
|
||||
)
|
||||
|
||||
|
||||
class TestSQRLURLValidator(unittest.TestCase):
|
||||
def test_valid(self):
|
||||
self.assertIsNone(
|
||||
SQRLURLValidator()('qrl://example.com:8000/sqrl/?nut=hello')
|
||||
)
|
||||
self.assertIsNone(
|
||||
SQRLURLValidator()('sqrl://example.com:8000/sqrl/?nut=hello')
|
||||
)
|
||||
|
||||
def test_invalid(self):
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
SQRLURLValidator()('http://example.com:Base64PairsField8000/sqrl/?nut=hello')
|
||||
|
||||
|
||||
class TestSQRLURLField(unittest.TestCase):
|
||||
def test_default_validators(self):
|
||||
validator_types = list(map(type, SQRLURLField.default_validators))
|
||||
self.assertIn(SQRLURLValidator, validator_types)
|
||||
|
||||
|
||||
class TestBase64Field(unittest.TestCase):
|
||||
def test_empty_value(self):
|
||||
value = Base64Field().to_python(None)
|
||||
|
||||
self.assertEqual(value, b'')
|
||||
self.assertIsInstance(value, bytes)
|
||||
|
||||
def test_value(self):
|
||||
value = Base64Field().to_python('aGVsbG8')
|
||||
|
||||
self.assertEqual(value, b'hello')
|
||||
self.assertIsInstance(value, bytes)
|
||||
|
||||
def test_value_invalid(self):
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
Base64Field().to_python('hello')
|
||||
|
||||
|
||||
class TestBase64CharField(unittest.TestCase):
|
||||
def test_empty_value(self):
|
||||
value = Base64CharField().to_python(None)
|
||||
|
||||
self.assertEqual(value, '')
|
||||
self.assertIsInstance(value, str)
|
||||
|
||||
def test_value(self):
|
||||
value = Base64CharField().to_python('aGVsbG8')
|
||||
|
||||
self.assertEqual(value, 'hello')
|
||||
self.assertIsInstance(value, str)
|
||||
|
||||
def test_value_invalid(self):
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
Base64CharField().to_python('z4A')
|
||||
|
||||
|
||||
class TestBase64PairsField(unittest.TestCase):
|
||||
def test_to_python_empty(self):
|
||||
value = Base64PairsField().to_python(None)
|
||||
|
||||
self.assertEqual(value, OrderedDict())
|
||||
|
||||
def test_to_python(self):
|
||||
value = Base64.encode(
|
||||
b'ver=1\r\n'
|
||||
b'foo=bar\r\n'
|
||||
)
|
||||
|
||||
value = Base64PairsField().to_python(value)
|
||||
|
||||
self.assertEqual(value, OrderedDict([
|
||||
('ver', '1'),
|
||||
('foo', 'bar'),
|
||||
]))
|
||||
|
||||
def test_to_python_not_pars(self):
|
||||
value = Base64.encode(
|
||||
b'ver=1\r\n'
|
||||
b'foo\r\n'
|
||||
)
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
Base64PairsField().to_python(value)
|
||||
|
||||
def test_to_python_not_multiline(self):
|
||||
value = Base64.encode(
|
||||
b'ver=1'
|
||||
)
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
Base64PairsField().to_python(value)
|
||||
|
||||
|
||||
class TestBase64ConditionalPairsField(unittest.TestCase):
|
||||
def test_to_python_not_pars(self):
|
||||
value = Base64.encode(
|
||||
b'foo'
|
||||
)
|
||||
|
||||
self.assertEqual(Base64ConditionalPairsField().to_python(value), 'foo')
|
||||
|
||||
|
||||
class TestTildeMultipleValuesField(unittest.TestCase):
|
||||
def test_empty(self):
|
||||
self.assertListEqual(TildeMultipleValuesField().to_python(None), [])
|
||||
|
||||
def test_to_python(self):
|
||||
self.assertListEqual(
|
||||
TildeMultipleValuesField().to_python('hello~world'),
|
||||
['hello', 'world']
|
||||
)
|
||||
|
||||
|
||||
class TestTildeMultipleValuesFieldChoiceField(unittest.TestCase):
|
||||
def test_valid(self):
|
||||
field = TildeMultipleValuesFieldChoiceField(choices=[
|
||||
('hello', 'hello'),
|
||||
('world', 'world'),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
field.clean('hello~world'),
|
||||
['hello', 'world']
|
||||
)
|
||||
|
||||
def test_invalid(self):
|
||||
field = TildeMultipleValuesFieldChoiceField(choices=[
|
||||
('hello', 'hello'),
|
||||
])
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
field.clean('hello~world')
|
641
sqrl/tests/test_forms.py
Normal file
641
sqrl/tests/test_forms.py
Normal file
|
@ -0,0 +1,641 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
|
||||
import ed25519
|
||||
import mock
|
||||
from django import forms, test
|
||||
from django.contrib.auth import SESSION_KEY, get_user_model
|
||||
from django.utils.timezone import now
|
||||
|
||||
from ..crypto import HMAC, Ed25519, generate_randomness
|
||||
from ..forms import PasswordLessUserCreationForm, RequestForm
|
||||
from ..models import SQRLIdentity, SQRLNut
|
||||
from ..utils import Base64, Encoder
|
||||
|
||||
|
||||
TESTING_MODULE = 'sqrl.forms'
|
||||
|
||||
|
||||
class TestRequestForm(test.TestCase):
|
||||
def get_key_pair(self):
|
||||
signing_key, verifying_key = ed25519.create_keypair()
|
||||
signing_key = signing_key.to_bytes()
|
||||
verifying_key = verifying_key.to_bytes()
|
||||
|
||||
return signing_key, verifying_key
|
||||
|
||||
def _setup(self):
|
||||
hasattr(self, 'nut') and self.nut.delete()
|
||||
hasattr(self, 'user') and self.user.delete()
|
||||
hasattr(self, 'identity') and self.identity.delete()
|
||||
|
||||
self.nut = SQRLNut(
|
||||
nonce=generate_randomness(),
|
||||
transaction_nonce=generate_randomness(),
|
||||
session_key=generate_randomness(20),
|
||||
is_transaction_complete=False,
|
||||
ip_address='127.0.0.1',
|
||||
timestamp=now(),
|
||||
)
|
||||
self.nut.save()
|
||||
|
||||
self.user = get_user_model().objects.create(
|
||||
username='test_clean_session',
|
||||
)
|
||||
|
||||
self.identity = SQRLIdentity(
|
||||
user_id=self.user.pk,
|
||||
public_key=Base64.encode(self.public_key),
|
||||
server_unlock_key=Base64.encode(self.server_unlock_key),
|
||||
verify_unlock_key=Base64.encode(self.verify_unlock_key),
|
||||
is_enabled=True,
|
||||
is_only_sqrl=False,
|
||||
)
|
||||
self.identity.save()
|
||||
|
||||
self.server_data = OrderedDict([
|
||||
('ver', 1),
|
||||
('nut', self.nut.nonce),
|
||||
('tif', '8'),
|
||||
('qry', '/sqrl/auth/?nut=nonce'),
|
||||
('sfn', 'Test Server'),
|
||||
])
|
||||
self.server_data['mac'] = HMAC(self.nut, self.server_data).sign_data()
|
||||
self.server_data = Encoder.normalize(self.server_data)
|
||||
|
||||
self.client_data = OrderedDict([
|
||||
('ver', 1),
|
||||
('cmd', self.cmd),
|
||||
('opt', ['sqrlonly']),
|
||||
])
|
||||
if self.include_idk:
|
||||
self.client_data['idk'] = self.public_key
|
||||
if self.include_pidk:
|
||||
self.client_data['pidk'] = self.previous_public_key
|
||||
if self.include_suk:
|
||||
self.client_data['suk'] = self.server_unlock_key
|
||||
if self.include_vuk:
|
||||
self.client_data['vuk'] = self.verify_unlock_key
|
||||
|
||||
self.payload_client_data = Encoder.normalize(OrderedDict(
|
||||
(k, v if not isinstance(v, list) else '~'.join(v))
|
||||
for k, v in self.client_data.items()
|
||||
))
|
||||
|
||||
self.data = {
|
||||
'client': Encoder.base64_dumps(self.client_data),
|
||||
'server': Encoder.base64_dumps(self.server_data),
|
||||
}
|
||||
self.signable_data = (
|
||||
self.data['client'] + self.data['server']
|
||||
).encode('ascii')
|
||||
if self.include_ids:
|
||||
self.data['ids'] = Ed25519(
|
||||
self.public_key, self.identity_key, self.signable_data
|
||||
).sign_data()
|
||||
if self.include_pids:
|
||||
self.data['pids'] = Ed25519(
|
||||
self.previous_public_key, self.previous_identity_key, self.signable_data
|
||||
).sign_data()
|
||||
if self.include_urs:
|
||||
self.data['urs'] = Ed25519(
|
||||
self.verify_unlock_key, self.unlock_key, self.signable_data
|
||||
).sign_data()
|
||||
|
||||
self.cleaned_data = self.data.copy()
|
||||
self.cleaned_data.update({
|
||||
'client': self.client_data,
|
||||
'server': self.server_data,
|
||||
})
|
||||
|
||||
self.form = RequestForm(self.nut, data=self.data)
|
||||
|
||||
def setUp(self):
|
||||
super(TestRequestForm, self).setUp()
|
||||
|
||||
self.cmd = ['query']
|
||||
|
||||
self.identity_key, self.public_key = self.get_key_pair()
|
||||
self.previous_identity_key, self.previous_public_key = self.get_key_pair()
|
||||
self.unlock_key, self.verify_unlock_key = self.get_key_pair()
|
||||
self.server_unlock_key = b'hello'
|
||||
|
||||
self.include_idk = True
|
||||
self.include_pidk = True
|
||||
self.include_suk = True
|
||||
self.include_vuk = True
|
||||
self.include_ids = True
|
||||
self.include_pids = True
|
||||
self.include_urs = True
|
||||
|
||||
self._setup()
|
||||
|
||||
def tearDown(self):
|
||||
self.user and self.user.delete()
|
||||
self.identity and self.identity.delete()
|
||||
self.nut and self.nut.delete()
|
||||
super(TestRequestForm, self).tearDown()
|
||||
|
||||
def test_init(self):
|
||||
form = RequestForm(mock.sentinel.nut)
|
||||
|
||||
self.assertEqual(form.nut, mock.sentinel.nut)
|
||||
self.assertIsNone(form.session)
|
||||
self.assertIsNone(form.identity)
|
||||
self.assertIsNone(form.previous_identity)
|
||||
|
||||
def test_clean_client(self):
|
||||
self.form.cleaned_data = {'client': self.payload_client_data}
|
||||
|
||||
self.assertEqual(self.form.clean_client(), dict(self.client_data))
|
||||
|
||||
def test_clean_client_invalid(self):
|
||||
self.form.cleaned_data = {
|
||||
'client': {
|
||||
'ver': '2',
|
||||
}
|
||||
}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form.clean_client()
|
||||
|
||||
def test_clean_server_not_dict(self):
|
||||
self.form.cleaned_data = {'server': mock.sentinel.server_data}
|
||||
|
||||
self.assertEqual(self.form.clean_server(), mock.sentinel.server_data)
|
||||
|
||||
def test_clean_server(self):
|
||||
self.form.cleaned_data = {
|
||||
'server': self.server_data
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
self.form.clean_server(),
|
||||
self.server_data
|
||||
)
|
||||
|
||||
def test_clean_server_mac_not_base64(self):
|
||||
self.server_data['mac'] = 'hello'
|
||||
self.form.cleaned_data = {'server': self.server_data}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form.clean_server()
|
||||
|
||||
def test_clean_server_mismatch_nut(self):
|
||||
self.server_data['nut'] = self.server_data['nut'][::-1]
|
||||
self.form.cleaned_data = {'server': self.server_data}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form.clean_server()
|
||||
|
||||
def test_clean_server_mismatch_missing_mac(self):
|
||||
del self.server_data['mac']
|
||||
self.form.cleaned_data = {'server': self.server_data}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form.clean_server()
|
||||
|
||||
def test_clean_server_invalid_mac(self):
|
||||
self.server_data['mac'] = self.server_data['mac'][::-1]
|
||||
self.form.cleaned_data = {'server': self.server_data}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form.clean_server()
|
||||
|
||||
def test_clean_ids(self):
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
'ids': self.data['ids'],
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
self.form.clean_ids(),
|
||||
self.data['ids']
|
||||
)
|
||||
|
||||
def test_clean_ids_invalid(self):
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
'ids': self.data['ids'][::-1],
|
||||
}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form.clean_ids()
|
||||
|
||||
def test_clean_pids_valid(self):
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
'pids': self.data['pids'],
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
self.form.clean_pids(),
|
||||
self.data['pids']
|
||||
)
|
||||
|
||||
def test_clean_pids_invalid(self):
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
'pids': self.data['pids'][::-1],
|
||||
}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form.clean_pids()
|
||||
|
||||
def test_clean_pids_missing_pids(self):
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form.clean_pids()
|
||||
|
||||
def test_clean_pids_missing_pidk(self):
|
||||
self.client_data.pop('pidk')
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
'pids': self.data['pids'],
|
||||
}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form.clean_pids()
|
||||
|
||||
def test_clean_urs(self):
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
self.form.identity = self.identity
|
||||
|
||||
self.assertEqual(
|
||||
self.form._clean_urs(),
|
||||
self.data['urs']
|
||||
)
|
||||
|
||||
def test_clean_urs_invalid(self):
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
'urs': self.data['urs'][::-1],
|
||||
}
|
||||
self.form.identity = self.identity
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_urs()
|
||||
|
||||
def test_clean_urs_no_suk(self):
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
'urs': self.data['urs'],
|
||||
}
|
||||
self.form.identity = self.identity
|
||||
self.identity.server_unlock_key = None
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_urs()
|
||||
|
||||
def test_clean_urs_no_vuk(self):
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
'urs': self.data['urs'],
|
||||
}
|
||||
self.form.identity = self.identity
|
||||
self.identity.verify_unlock_key = None
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_urs()
|
||||
|
||||
def test_clean_cmd_query(self):
|
||||
self.cmd = ['query']
|
||||
self._setup()
|
||||
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
}
|
||||
|
||||
self.assertIsNone(self.form._clean_client_cmd())
|
||||
|
||||
def test_clean_cmd_ident(self):
|
||||
self.cmd = ['ident']
|
||||
self._setup()
|
||||
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
}
|
||||
|
||||
self.assertIsNone(self.form._clean_client_cmd())
|
||||
|
||||
def test_clean_cmd_ident_no_suk_vuk_without_identity(self):
|
||||
self.cmd = ['ident']
|
||||
self.include_suk = None
|
||||
self.include_vuk = None
|
||||
self._setup()
|
||||
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_client_cmd()
|
||||
|
||||
def test_clean_cmd_ident_suk_vuk_with_identity(self):
|
||||
self.cmd = ['ident']
|
||||
self._setup()
|
||||
|
||||
self.form.identity = self.identity
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_client_cmd()
|
||||
|
||||
def test_clean_cmd_ident_with_disable(self):
|
||||
self.cmd = ['ident', 'disable']
|
||||
self._setup()
|
||||
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_client_cmd()
|
||||
|
||||
def test_clean_cmd_ident_no_urs_with_previous_identity(self):
|
||||
self.cmd = ['ident']
|
||||
self._setup()
|
||||
|
||||
self.form.previous_identity = self.identity
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_client_cmd()
|
||||
|
||||
def test_clean_cmd_disable(self):
|
||||
self.cmd = ['disable']
|
||||
self._setup()
|
||||
|
||||
self.form.identity = self.identity
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
}
|
||||
|
||||
self.assertIsNone(self.form._clean_client_cmd())
|
||||
|
||||
def test_clean_cmd_disable_no_identity(self):
|
||||
self.cmd = ['disable']
|
||||
self._setup()
|
||||
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_client_cmd()
|
||||
|
||||
def test_clean_cmd_disable_with_enable(self):
|
||||
self.cmd = ['disable', 'enable']
|
||||
self._setup()
|
||||
|
||||
self.form.identity = self.identity
|
||||
self.form.cleaned_data = {
|
||||
'client': self.client_data,
|
||||
}
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_client_cmd()
|
||||
|
||||
def test_clean_cmd_enable(self):
|
||||
self.cmd = ['enable']
|
||||
self._setup()
|
||||
|
||||
self.form.identity = self.identity
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
|
||||
self.assertIsNone(self.form._clean_client_cmd())
|
||||
|
||||
def test_clean_cmd_enable_no_urs(self):
|
||||
self.cmd = ['enable']
|
||||
self.include_urs = False
|
||||
self._setup()
|
||||
|
||||
self.form.identity = self.identity
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_client_cmd()
|
||||
|
||||
def test_clean_cmd_enable_no_identity(self):
|
||||
self.cmd = ['enable']
|
||||
self._setup()
|
||||
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_client_cmd()
|
||||
|
||||
def test_clean_cmd_enable_with_disable(self):
|
||||
self.cmd = ['enable', 'disable']
|
||||
self._setup()
|
||||
|
||||
self.form.identity = self.identity
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_client_cmd()
|
||||
|
||||
def test_clean_cmd_remove(self):
|
||||
self.cmd = ['remove']
|
||||
self._setup()
|
||||
|
||||
self.form.identity = self.identity
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
|
||||
self.assertIsNone(self.form._clean_client_cmd())
|
||||
|
||||
def test_clean_cmd_remove_no_identity(self):
|
||||
self.cmd = ['remove']
|
||||
self._setup()
|
||||
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_client_cmd()
|
||||
|
||||
def test_clean_cmd_remove_no_urs(self):
|
||||
self.cmd = ['remove']
|
||||
self.include_urs = False
|
||||
self._setup()
|
||||
|
||||
self.form.identity = self.identity
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_client_cmd()
|
||||
|
||||
def test_clean_cmd_remove_with_other_cmd(self):
|
||||
self.cmd = ['remove', 'ident']
|
||||
self._setup()
|
||||
|
||||
self.form.identity = self.identity
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_client_cmd()
|
||||
|
||||
def test_clean_session_empty(self):
|
||||
self.form.session = {}
|
||||
|
||||
self.assertIsNone(self.form._clean_session())
|
||||
|
||||
def test_clean_session_user_not_found(self):
|
||||
assert not get_user_model().objects.filter(pk=1000).first()
|
||||
|
||||
self.form.session = {
|
||||
SESSION_KEY: '1000',
|
||||
}
|
||||
|
||||
self.assertIsNone(self.form._clean_session())
|
||||
|
||||
def test_clean_session_user_not_int(self):
|
||||
self.form.session = {
|
||||
SESSION_KEY: 'aaa',
|
||||
}
|
||||
|
||||
self.assertIsNone(self.form._clean_session())
|
||||
|
||||
def test_clean_session_no_sqrl_identity(self):
|
||||
self.identity.delete()
|
||||
self.identity = None
|
||||
self.form.session = {
|
||||
SESSION_KEY: str(self.user.pk),
|
||||
}
|
||||
|
||||
self.assertIsNone(self.form._clean_session())
|
||||
|
||||
def test_clean_session_public_key_not_matches(self):
|
||||
self.identity.public_key = self.identity.public_key[::-1]
|
||||
self.identity.save()
|
||||
|
||||
self.form.session = {
|
||||
SESSION_KEY: str(self.user.pk),
|
||||
}
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_session()
|
||||
|
||||
def test_clean_session_user_code_mismatch(self):
|
||||
self.form.identity = self.identity
|
||||
self.form.session = {
|
||||
SESSION_KEY: str(self.user.pk + 1),
|
||||
}
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.form._clean_session()
|
||||
|
||||
def test_clean(self):
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
|
||||
actual = self.form.clean()
|
||||
|
||||
self.assertEqual(actual, self.cleaned_data)
|
||||
|
||||
@mock.patch.object(RequestForm, 'find_identities')
|
||||
@mock.patch.object(RequestForm, '_clean_client_cmd')
|
||||
@mock.patch.object(RequestForm, '_clean_urs')
|
||||
@mock.patch.object(RequestForm, 'find_session')
|
||||
@mock.patch.object(RequestForm, '_clean_session')
|
||||
@mock.patch.object(forms.Form, 'clean')
|
||||
def test_clean_mock(self,
|
||||
mock_super_clean,
|
||||
mock_clean_session,
|
||||
mock_find_session,
|
||||
mock_clean_urs,
|
||||
mock_clean_client_cmd,
|
||||
mock_find_identities):
|
||||
mock_super_clean.return_value = mock.sentinel.cleaned_data
|
||||
|
||||
actual = self.form.clean()
|
||||
|
||||
self.assertEqual(actual, mock.sentinel.cleaned_data)
|
||||
mock_super_clean.assert_called_once_with()
|
||||
mock_clean_session.assert_called_once_with()
|
||||
mock_find_session.assert_called_once_with()
|
||||
mock_clean_urs.assert_called_once_with()
|
||||
mock_clean_client_cmd.assert_called_once_with()
|
||||
mock_find_identities.assert_called_once_with()
|
||||
|
||||
@mock.patch(TESTING_MODULE + '.SessionMiddleware')
|
||||
def test_find_session(self, mock_session_middleware):
|
||||
self.form.find_session()
|
||||
|
||||
self.assertEqual(
|
||||
self.form.session,
|
||||
mock_session_middleware.return_value.SessionStore.return_value
|
||||
)
|
||||
mock_session_middleware.return_value.SessionStore.assert_called_once_with(
|
||||
self.nut.session_key,
|
||||
)
|
||||
|
||||
def test_find_identities(self):
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
|
||||
self.form.find_identities()
|
||||
|
||||
self.assertIsNotNone(self.form.identity)
|
||||
self.assertIsInstance(self.form.identity, SQRLIdentity)
|
||||
self.assertEqual(self.form.identity.public_key, self.identity.public_key)
|
||||
self.assertIsNone(self.form.previous_identity)
|
||||
|
||||
@mock.patch.object(RequestForm, '_get_identity')
|
||||
def test_find_identities_mock(self, mock_get_identity):
|
||||
self.form.cleaned_data = self.cleaned_data
|
||||
mock_get_identity.side_effect = mock.sentinel.identity, mock.sentinel.previous_identity
|
||||
|
||||
self.form.find_identities()
|
||||
|
||||
self.assertEqual(self.form.identity, mock.sentinel.identity)
|
||||
self.assertEqual(self.form.previous_identity, mock.sentinel.previous_identity)
|
||||
mock_get_identity.assert_has_calls([
|
||||
mock.call(self.public_key),
|
||||
mock.call(self.previous_public_key),
|
||||
])
|
||||
|
||||
@mock.patch(TESTING_MODULE + '.SQRLIdentity')
|
||||
def test_get_identity(self, mock_sqrl_identity):
|
||||
actual = self.form._get_identity(self.public_key)
|
||||
|
||||
self.assertEqual(
|
||||
actual,
|
||||
mock_sqrl_identity.objects.filter.return_value.first.return_value
|
||||
)
|
||||
mock_sqrl_identity.objects.filter.assert_called_once_with(
|
||||
public_key=Base64.encode(self.public_key)
|
||||
)
|
||||
|
||||
def test_get_identity_no_key(self):
|
||||
self.assertIsNone(self.form._get_identity(None))
|
||||
|
||||
|
||||
class TestRandomPasswordUserCreationForm(unittest.TestCase):
|
||||
def test_init(self):
|
||||
self.assertIn('password1', PasswordLessUserCreationForm.base_fields)
|
||||
self.assertIn('password2', PasswordLessUserCreationForm.base_fields)
|
||||
|
||||
form = PasswordLessUserCreationForm()
|
||||
|
||||
self.assertNotIn('password1', form.fields)
|
||||
self.assertNotIn('password2', form.fields)
|
||||
|
||||
def test_save(self):
|
||||
form = PasswordLessUserCreationForm({'username': 'test'})
|
||||
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
user = form.save()
|
||||
|
||||
self.assertEqual(user.username, 'test')
|
||||
self.assertTrue(user.password.startswith('!'))
|
||||