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('!'))
|
||||
|
||||
user.delete()
|
32
sqrl/tests/test_managers.py
Normal file
32
sqrl/tests/test_managers.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from ..managers import SQRLNutManager
|
||||
|
||||
|
||||
class TestSQRLNutManager(unittest.TestCase):
|
||||
@mock.patch.object(SQRLNutManager, 'get_queryset')
|
||||
@mock.patch.object(SQRLNutManager, 'create')
|
||||
def test_replace_or_create(self, mock_create, mock_getqueryset):
|
||||
actual = SQRLNutManager().replace_or_create(
|
||||
session_key=mock.sentinel.session_key,
|
||||
nonce=mock.sentinel.nonce,
|
||||
transaction_nonce=mock.sentinel.transaction_nonce,
|
||||
is_transaction_complete=False,
|
||||
ip_address=mock.sentinel.ip_address,
|
||||
)
|
||||
|
||||
self.assertEqual(actual, mock_create.return_value)
|
||||
mock_getqueryset.return_value.filter.assert_called_once_with(
|
||||
session_key=mock.sentinel.session_key
|
||||
)
|
||||
mock_getqueryset.return_value.filter.return_value.delete.assert_called_once_with()
|
||||
mock_create.assert_called_once_with(
|
||||
session_key=mock.sentinel.session_key,
|
||||
nonce=mock.sentinel.nonce,
|
||||
transaction_nonce=mock.sentinel.transaction_nonce,
|
||||
is_transaction_complete=False,
|
||||
ip_address=mock.sentinel.ip_address,
|
||||
)
|
32
sqrl/tests/test_models.py
Normal file
32
sqrl/tests/test_models.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from ..managers import SQRLNutManager
|
||||
from ..models import SQRLNut
|
||||
|
||||
|
||||
TESTING_MODULE = 'sqrl.models'
|
||||
|
||||
|
||||
class TestSQRLNut(unittest.TestCase):
|
||||
def test_objects(self):
|
||||
self.assertIsInstance(SQRLNut.objects, SQRLNutManager)
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(
|
||||
str(SQRLNut(nonce='nonce')),
|
||||
'nonce'
|
||||
)
|
||||
|
||||
@mock.patch(TESTING_MODULE + '.generate_randomness')
|
||||
@mock.patch.object(SQRLNut, 'delete')
|
||||
@mock.patch.object(SQRLNut, 'save')
|
||||
def test_renew(self, mock_save, mock_delete, mock_generate_randomness):
|
||||
nut = SQRLNut(nonce='nonce')
|
||||
|
||||
self.assertIsNone(nut.renew())
|
||||
self.assertEqual(nut.nonce, mock_generate_randomness.return_value)
|
||||
mock_save.assert_called_once_with()
|
||||
mock_generate_randomness.assert_called_once_with()
|
63
sqrl/tests/test_response.py
Normal file
63
sqrl/tests/test_response.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
|
||||
import mock
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from ..crypto import HMAC
|
||||
from ..response import SQRLHttpResponse
|
||||
from ..utils import Encoder
|
||||
|
||||
|
||||
TESTING_MODULE = 'sqrl.response'
|
||||
|
||||
|
||||
class TestSQRLHttpResponse(unittest.TestCase):
|
||||
def setUp(self):
|
||||
super(TestSQRLHttpResponse, self).setUp()
|
||||
self.data = OrderedDict([
|
||||
('ver', 1),
|
||||
('nut', b'nonce'),
|
||||
('tif', '8'),
|
||||
('qry', '/sqrl/auth/?nut=nonce'),
|
||||
('sfn', 'Test Server'),
|
||||
])
|
||||
self.nut = mock.MagicMock(session_key='session')
|
||||
|
||||
@override_settings(DEBUG=False)
|
||||
def test_response(self):
|
||||
response = SQRLHttpResponse(self.nut, self.data)
|
||||
|
||||
expected_data = self.data.copy()
|
||||
expected_data['mac'] = HMAC(self.nut, self.data).sign_data()
|
||||
|
||||
self.assertEqual(
|
||||
response.content,
|
||||
Encoder.base64_dumps(expected_data).encode('ascii')
|
||||
)
|
||||
self.assertEqual(response['Content-Length'], str(len(response.content)))
|
||||
self.assertEqual(response['Content-Type'], 'application/sqrl')
|
||||
|
||||
@override_settings(DEBUG=False)
|
||||
def test_response_without_nut(self):
|
||||
response = SQRLHttpResponse(None, self.data)
|
||||
|
||||
self.assertEqual(
|
||||
response.content,
|
||||
Encoder.base64_dumps(self.data).encode('ascii')
|
||||
)
|
||||
self.assertEqual(response['Content-Length'], str(len(response.content)))
|
||||
self.assertEqual(response['Content-Type'], 'application/sqrl')
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@mock.patch(TESTING_MODULE + '.log')
|
||||
def test_response_debug(self, mock_log):
|
||||
response = SQRLHttpResponse(self.nut, self.data)
|
||||
|
||||
self.assertEqual(response['X-SQRL-ver'], '1')
|
||||
self.assertEqual(response['X-SQRL-nut'], 'bm9uY2U')
|
||||
self.assertEqual(response['X-SQRL-tif'], '8')
|
||||
self.assertEqual(response['X-SQRL-qry'], '/sqrl/auth/?nut=nonce')
|
||||
self.assertEqual(response['X-SQRL-sfn'], 'Test Server')
|
||||
self.assertIn('X-SQRL-mac', response)
|
116
sqrl/tests/test_sqrl.py
Normal file
116
sqrl/tests/test_sqrl.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from sqrl.managers import SQRLNutManager
|
||||
|
||||
from ..sqrl import SQRLInitialization
|
||||
|
||||
|
||||
TESTING_MODULE = 'sqrl.sqrl'
|
||||
|
||||
|
||||
class TestSQRLInitialization(unittest.TestCase):
|
||||
def test_init(self):
|
||||
sqrl = SQRLInitialization(mock.sentinel.request, mock.sentinel.nut)
|
||||
|
||||
self.assertEqual(sqrl.request, mock.sentinel.request)
|
||||
self.assertEqual(sqrl.nut, mock.sentinel.nut)
|
||||
|
||||
def test_get_or_create_session_key_exists(self):
|
||||
m = mock.MagicMock()
|
||||
self.assertEqual(
|
||||
SQRLInitialization(m).get_or_create_session_key(),
|
||||
m.session.session_key
|
||||
)
|
||||
self.assertFalse(m.session.create.called)
|
||||
|
||||
def test_get_or_create_session_key_create(self):
|
||||
m = mock.MagicMock()
|
||||
m.session.session_key = None
|
||||
|
||||
self.assertEqual(
|
||||
SQRLInitialization(m).get_or_create_session_key(),
|
||||
m.session.session_key
|
||||
)
|
||||
m.session.create.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(SQRLInitialization, 'generate_nut_kwargs')
|
||||
@mock.patch.object(SQRLNutManager, 'replace_or_create')
|
||||
def test_nut(self, mock_replace_or_create, mock_generate_nut_kwargs):
|
||||
mock_generate_nut_kwargs.return_value = {
|
||||
'foo': 'bar'
|
||||
}
|
||||
|
||||
sqrl = SQRLInitialization(None)
|
||||
|
||||
self.assertEqual(sqrl.nut, mock_replace_or_create.return_value)
|
||||
mock_replace_or_create.assert_called_once_with(foo='bar')
|
||||
mock_generate_nut_kwargs.assert_called_once_with()
|
||||
|
||||
def test_nut_setter(self):
|
||||
sqrl = SQRLInitialization(None)
|
||||
|
||||
# sanity check
|
||||
self.assertFalse(hasattr(sqrl, '_nut'))
|
||||
|
||||
sqrl.nut = mock.sentinel.nut
|
||||
|
||||
self.assertTrue(hasattr(sqrl, '_nut'))
|
||||
self.assertEqual(sqrl._nut, mock.sentinel.nut)
|
||||
self.assertEqual(sqrl.nut, mock.sentinel.nut)
|
||||
|
||||
@mock.patch.object(SQRLInitialization, 'get_or_create_session_key')
|
||||
@mock.patch(TESTING_MODULE + '.get_user_ip')
|
||||
@mock.patch(TESTING_MODULE + '.generate_randomness')
|
||||
def test_generate_nut_kwargs(self, mock_generate_randomness, mock_get_user_ip,
|
||||
mock_get_or_create_session_key):
|
||||
mock_generate_randomness.return_value = 'abc123'
|
||||
|
||||
actual = SQRLInitialization(mock.sentinel.request).generate_nut_kwargs()
|
||||
|
||||
self.assertDictEqual(
|
||||
actual, {
|
||||
'session_key': mock_get_or_create_session_key.return_value,
|
||||
'nonce': 'abc',
|
||||
'transaction_nonce': '123',
|
||||
'is_transaction_complete': False,
|
||||
'ip_address': mock_get_user_ip.return_value,
|
||||
}
|
||||
)
|
||||
mock_generate_randomness.assert_called_once_with(64)
|
||||
mock_get_user_ip.assert_called_once_with(mock.sentinel.request)
|
||||
|
||||
@mock.patch(TESTING_MODULE + '.reverse')
|
||||
def test_get_sqrl_url(self, mock_reverse):
|
||||
actual = SQRLInitialization(None).get_sqrl_url()
|
||||
|
||||
self.assertEqual(actual, mock_reverse.return_value)
|
||||
|
||||
def test_get_sqrl_url_params(self):
|
||||
actual = SQRLInitialization(None, mock.MagicMock(nonce='foo&bar')).get_sqrl_url_params()
|
||||
|
||||
self.assertEqual(actual, 'nut=foo%26bar')
|
||||
|
||||
@mock.patch.object(SQRLInitialization, 'get_sqrl_url_params')
|
||||
@mock.patch.object(SQRLInitialization, 'get_sqrl_url')
|
||||
def test_url(self, mock_get_sqrl_url, mock_get_sqrl_url_params):
|
||||
mock_get_sqrl_url.return_value = '/sqrl/auth/'
|
||||
mock_get_sqrl_url_params.return_value = 'nut=nonce'
|
||||
|
||||
actual = SQRLInitialization(None).url
|
||||
|
||||
self.assertEqual(actual, '/sqrl/auth/?nut=nonce')
|
||||
|
||||
@mock.patch.object(SQRLInitialization, 'url', new_callable=mock.PropertyMock)
|
||||
def test_sqrl_url(self, mock_url):
|
||||
mock_url.return_value = '/sqrl/auth/?nut=nonce'
|
||||
|
||||
request = mock.MagicMock()
|
||||
request.is_secure.return_value = True
|
||||
request.get_host.return_value = 'example.com:8000'
|
||||
|
||||
actual = SQRLInitialization(request).sqrl_url
|
||||
|
||||
self.assertEqual(actual, 'sqrl://example.com:8000/sqrl/auth/?nut=nonce')
|
7
sqrl/tests/test_urls.py
Normal file
7
sqrl/tests/test_urls.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .. import urls # noqa
|
||||
|
||||
|
||||
# nothing to test here however simply importing
|
||||
# url module evals urls so coverage is higher
|
80
sqrl/tests/test_utils.py
Normal file
80
sqrl/tests/test_utils.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
|
||||
import mock
|
||||
|
||||
from ..utils import Base64, Encoder, get_user_ip
|
||||
|
||||
|
||||
TESTING_MODULE = 'sqrl.utils'
|
||||
|
||||
|
||||
class TestBase64(unittest.TestCase):
|
||||
def test_encode(self):
|
||||
value = Base64.encode(b'hello')
|
||||
|
||||
# normal base64 is aGVsbG8= however = should be missing
|
||||
self.assertEqual(value, 'aGVsbG8')
|
||||
self.assertIsInstance(value, str)
|
||||
|
||||
def test_encode_not_binary(self):
|
||||
with self.assertRaises(AssertionError):
|
||||
Base64.encode('hello')
|
||||
|
||||
def test_decode(self):
|
||||
value = Base64.decode('aGVsbG8')
|
||||
|
||||
# normal base64 is aGVsbG8= however = should be missing
|
||||
self.assertEqual(value, b'hello')
|
||||
self.assertIsInstance(value, bytes)
|
||||
|
||||
def test_decode_not_unicode(self):
|
||||
with self.assertRaises(AssertionError):
|
||||
Base64.decode(b'aGVsbG8')
|
||||
|
||||
|
||||
class TestEncode(unittest.TestCase):
|
||||
def test_base64_dumps(self):
|
||||
self.assertEqual(Encoder.base64_dumps(5), '5')
|
||||
self.assertEqual(Encoder.base64_dumps('hello'), 'hello')
|
||||
self.assertEqual(Encoder.base64_dumps(b'hello'), 'aGVsbG8')
|
||||
self.assertEqual(Encoder.base64_dumps([b'hello', 'hello']), 'aGVsbG8~hello')
|
||||
self.assertEqual(
|
||||
Encoder.base64_dumps(OrderedDict([('a', b'hello'), ('b', 'hello')])),
|
||||
'YT1hR1ZzYkc4DQpiPWhlbGxvDQo'
|
||||
)
|
||||
|
||||
def test_dumps(self):
|
||||
self.assertEqual(Encoder.dumps(5), '5')
|
||||
self.assertEqual(Encoder.dumps('hello'), 'hello')
|
||||
self.assertEqual(Encoder.dumps(b'hello'), 'aGVsbG8')
|
||||
self.assertEqual(Encoder.dumps([b'hello', 'hello']), 'aGVsbG8~hello')
|
||||
self.assertEqual(Encoder.dumps(OrderedDict()), '')
|
||||
self.assertEqual(
|
||||
Encoder.dumps(OrderedDict([('a', b'hello'), ('b', 'hello')])),
|
||||
'a=aGVsbG8\r\nb=hello\r\n'
|
||||
)
|
||||
|
||||
def test_normalize(self):
|
||||
self.assertEqual(Encoder.normalize(b'hello'), 'aGVsbG8')
|
||||
self.assertEqual(Encoder.normalize('hello'), 'hello')
|
||||
self.assertEqual(Encoder.normalize(5), '5')
|
||||
self.assertEqual(Encoder.normalize([b'hello', 'hello']), ['aGVsbG8', 'hello'])
|
||||
self.assertEqual(Encoder.normalize(OrderedDict()), '')
|
||||
self.assertEqual(
|
||||
Encoder.normalize(OrderedDict([('a', b'hello'), ('b', 'hello')])),
|
||||
OrderedDict([('a', 'aGVsbG8'), ('b', 'hello')])
|
||||
)
|
||||
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
def test_get_user_ip(self):
|
||||
request = mock.MagicMock(META={'REMOTE_ADDR': mock.sentinel.ip})
|
||||
|
||||
self.assertEqual(get_user_ip(request), mock.sentinel.ip)
|
||||
|
||||
def test_get_user_ip_proxy(self):
|
||||
request = mock.MagicMock(META={'HTTP_X_REAL_IP': mock.sentinel.ip})
|
||||
|
||||
self.assertEqual(get_user_ip(request), mock.sentinel.ip)
|
194
sqrl/tests/test_views.py
Normal file
194
sqrl/tests/test_views.py
Normal file
|
@ -0,0 +1,194 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from django import test
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import serializers
|
||||
from django.urls import reverse
|
||||
from django.http import Http404, QueryDict
|
||||
from django.views.generic import FormView
|
||||
|
||||
from ..models import SQRLIdentity, SQRLNut
|
||||
from ..views import (
|
||||
SQRL_IDENTITY_SESSION_KEY,
|
||||
SQRLCompleteRegistrationView,
|
||||
SQRLStatusView,
|
||||
)
|
||||
|
||||
|
||||
TESTING_MODULE = 'sqrl.views'
|
||||
|
||||
|
||||
class TestSQRLStatusView(test.TestCase):
|
||||
def setUp(self):
|
||||
super(TestSQRLStatusView, self).setUp()
|
||||
self.view = SQRLStatusView()
|
||||
self.view.request = mock.MagicMock(GET={})
|
||||
self.view.request.is_ajax.return_value = True
|
||||
self.view.kwargs = {
|
||||
'transaction': '123',
|
||||
}
|
||||
|
||||
self.nut = SQRLNut.objects.create(
|
||||
nonce='hello',
|
||||
transaction_nonce='123',
|
||||
ip_address='127.0.0.1',
|
||||
is_transaction_complete=True,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.nut and self.nut.delete()
|
||||
super(TestSQRLStatusView, self).tearDown()
|
||||
|
||||
def test_get_success_url(self):
|
||||
self.assertEqual(
|
||||
self.view.get_success_url(),
|
||||
settings.LOGIN_REDIRECT_URL
|
||||
)
|
||||
|
||||
def test_get_success_url_from_querystring(self):
|
||||
self.view.request.GET['url'] = '?next={}'.format(reverse('sqrl:login'))
|
||||
|
||||
self.assertEqual(
|
||||
self.view.get_success_url(),
|
||||
reverse('sqrl:login'),
|
||||
)
|
||||
|
||||
def test_get_success_url_complete_registration(self):
|
||||
self.view.request.GET['url'] = '?next={}'.format(reverse('sqrl:login'))
|
||||
self.view.request.user.is_authenticated.return_value = False
|
||||
self.view.request.session = {SQRL_IDENTITY_SESSION_KEY: ''}
|
||||
|
||||
self.assertEqual(
|
||||
self.view.get_success_url(),
|
||||
reverse('sqrl:complete-registration') + '?next={}'.format(reverse('sqrl:login')),
|
||||
)
|
||||
|
||||
def test_get_object_404(self):
|
||||
self.nut.delete()
|
||||
self.nut = None
|
||||
|
||||
with self.assertRaises(Http404):
|
||||
self.view.get_object()
|
||||
|
||||
def test_get_object(self):
|
||||
actual = self.view.get_object()
|
||||
|
||||
self.assertIsInstance(actual, SQRLNut)
|
||||
self.assertEqual(actual.nonce, self.nut.nonce)
|
||||
self.assertEqual(actual.transaction_nonce, self.nut.transaction_nonce)
|
||||
|
||||
def test_post(self):
|
||||
response = self.view.post(self.view.request, self.nut.transaction_nonce)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['content-type'], 'application/json')
|
||||
self.assertEqual(json.loads(response.content.decode('utf-8')), {
|
||||
'transaction_complete': True,
|
||||
'redirect_to': settings.LOGIN_REDIRECT_URL,
|
||||
})
|
||||
|
||||
def test_post_not_ajax(self):
|
||||
self.view.request.is_ajax.return_value = False
|
||||
|
||||
response = self.view.post(self.view.request, self.nut.transaction_nonce)
|
||||
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
|
||||
class TestSQRLAuthView(test.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class TestCompleteRegistrationView(test.TestCase):
|
||||
def setUp(self):
|
||||
super(TestCompleteRegistrationView, self).setUp()
|
||||
self.view = SQRLCompleteRegistrationView()
|
||||
self.username = 'foobartest'
|
||||
self.view.request = mock.MagicMock(
|
||||
method='POST',
|
||||
POST={
|
||||
'username': self.username,
|
||||
},
|
||||
)
|
||||
self.identity = SQRLIdentity(
|
||||
public_key='a' * 43,
|
||||
verify_unlock_key='b' * 43,
|
||||
server_unlock_key='c' * 43,
|
||||
is_enabled=True,
|
||||
is_only_sqrl=False,
|
||||
)
|
||||
self.view.request.session = {
|
||||
SQRL_IDENTITY_SESSION_KEY: serializers.serialize('json', [self.identity]),
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
SQRLIdentity.objects.filter(public_key=self.identity.public_key).delete()
|
||||
get_user_model().objects.filter(username=self.username).delete()
|
||||
super(TestCompleteRegistrationView, self).tearDown()
|
||||
|
||||
def test_check_session_for_sqrl_identity_or_404(self):
|
||||
self.assertIsNone(self.view.check_session_for_sqrl_identity_or_404())
|
||||
|
||||
def test_check_session_for_sqrl_identity_or_404_raises(self):
|
||||
self.view.request.session = {}
|
||||
|
||||
with self.assertRaises(Http404):
|
||||
self.view.check_session_for_sqrl_identity_or_404()
|
||||
|
||||
@mock.patch.object(SQRLCompleteRegistrationView, 'check_session_for_sqrl_identity_or_404')
|
||||
@mock.patch.object(FormView, 'get')
|
||||
def test_get(self, mock_super_get, mock_check_session_for_sqrl_identity_or_404):
|
||||
response = self.view.get(self.view.request)
|
||||
|
||||
self.assertEqual(response, mock_super_get.return_value)
|
||||
mock_check_session_for_sqrl_identity_or_404.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(SQRLCompleteRegistrationView, 'check_session_for_sqrl_identity_or_404')
|
||||
@mock.patch.object(FormView, 'post')
|
||||
def test_post(self, mock_super_post, mock_check_session_for_sqrl_identity_or_404):
|
||||
response = self.view.post(self.view.request)
|
||||
|
||||
self.assertEqual(response, mock_super_post.return_value)
|
||||
mock_check_session_for_sqrl_identity_or_404.assert_called_once_with()
|
||||
|
||||
def test_get_success_url(self):
|
||||
self.assertEqual(
|
||||
self.view.get_success_url(),
|
||||
settings.LOGIN_REDIRECT_URL
|
||||
)
|
||||
|
||||
def test_get_success_url_from_querystring(self):
|
||||
self.view.request.GET = {'next': reverse('sqrl:manage')}
|
||||
|
||||
self.assertEqual(
|
||||
self.view.get_success_url(),
|
||||
reverse('sqrl:manage'),
|
||||
)
|
||||
|
||||
@mock.patch(TESTING_MODULE + '.login')
|
||||
def test_form_valid(self, mock_login):
|
||||
form = self.view.get_form(self.view.get_form_class())
|
||||
form.is_valid()
|
||||
|
||||
# sanity checks
|
||||
self.assertFalse(get_user_model().objects.filter(username=self.username).count())
|
||||
self.assertFalse(SQRLIdentity.objects.filter(public_key=self.identity.public_key).count())
|
||||
|
||||
response = self.view.form_valid(form)
|
||||
user = get_user_model().objects.filter(username=self.username).first()
|
||||
|
||||
self.assertIsNotNone(user)
|
||||
self.assertEqual(user.username, self.username)
|
||||
self.assertIsInstance(user.sqrl_identity, SQRLIdentity)
|
||||
self.assertEqual(user.sqrl_identity.public_key, self.identity.public_key)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_form_valid_could_not_decode_identity(self):
|
||||
self.view.request.session[SQRL_IDENTITY_SESSION_KEY] = ''
|
||||
response = self.view.form_valid(self.view.get_form(self.view.get_form_class()))
|
||||
|
||||
self.assertEqual(response.status_code, 500)
|
|
@ -9,10 +9,12 @@ from .views import (
|
|||
SQRLStatusView,
|
||||
)
|
||||
|
||||
app_name = "sqrl"
|
||||
|
||||
urlpatterns = [
|
||||
path("auth/", SQRLAuthView.as_view(), name="auth"),
|
||||
path("login/", SQRLLoginView.as_view(), name="login"),
|
||||
path("manage/", SQRLIdentityManagementView.as_view(), name='manage')
|
||||
path("manage/", SQRLIdentityManagementView.as_view(), name='manage'),
|
||||
path("register/",SQRLCompleteRegistrationView.as_view(), name='complete-registration'),
|
||||
re_path(r"^status/(?P<transaction>[A-Za-z0-9_-]{43})/$", SQRLStatusView.as_view(), name='status'),
|
||||
]
|
||||
|
|
|
@ -18,7 +18,7 @@ class Base64(object):
|
|||
s: bytes
|
||||
Bytes string to be encoded as base64
|
||||
"""
|
||||
assert isinstance(s, (bites, bytearray))
|
||||
assert isinstance(s, (bytes, bytearray))
|
||||
return urlsafe_b64encode(s).decode('ascii').rstrip('=')
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -13,7 +13,7 @@ from django.contrib.auth import (
|
|||
login,
|
||||
)
|
||||
from django.core import serializers
|
||||
from django.url import reverse
|
||||
from django.urls import reverse
|
||||
from django.http import Http404, HttpResponse, JsonResponse, QueryDict
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views.generic import FormView, TemplateView, View
|
||||
|
@ -23,7 +23,6 @@ from .exceptions import TIF, TIFException
|
|||
from .forms import (
|
||||
AuthQueryDictForm,
|
||||
ExtractedNextUrlForm,
|
||||
GenerateQRForm,
|
||||
NextUrlForm,
|
||||
PasswordLessUserCreationForm,
|
||||
RequestForm,
|
||||
|
@ -31,7 +30,7 @@ from .forms import (
|
|||
from .models import SQRLIdentity, SQRLNut
|
||||
from .response import SQRLHttpResponse
|
||||
from .sqrl import SQRLInitialization
|
||||
from .utils import Base64, QRGenerator, get_user_ip
|
||||
from .utils import Base64, get_user_ip
|
||||
|
||||
|
||||
SQRL_IDENTITY_SESSION_KEY = '_sqrl_identity'
|
||||
|
|
Loading…
Reference in a new issue