Test QRCODE Svg in Django
date: 2021-04-19
Introduction
I worked out a solution in django-testing, for testing a view that renders a qrcode as an svg as an inline svg xml string.
In case you are not familiar with svg, scalable vector graphics, it is an image that is completely rendered from a string of text, or more accurately XML text, unlike PNG or JPG, which are binary files.
Python Libraries Used
- BeautifulSoup to consume html
- CairoSVG to convert svg to png
- python-pillow to convert transparent png background to white
- opencv-python to extract data from qrcode
- python-pyotp
Form ScreenShot
This is the form we will be testing.
In real life you would confirm that you want to use two-factor-authentication by scanning the qrcode with an authentication app on your smartphone, and then enter the time-based one-time password.
totp codes are derived from two things and two things only. A secret key and a time such as the current time. For instance, an authentication application can tell you what your totp code is, but of course in this testing scenario we use python-pyotp, after extracting the secret key from the qrcode.
Import Python Libraries
# tp/accounts/tests/test_enable_totp_view.py
# python manage.py test accounts.tests.test_enable_totp_view
from django.test import TestCase
from django.contrib.auth.models import User
from accounts.models import Account
from django.urls import reverse
from bs4 import BeautifulSoup
import cv2
from cairosvg import svg2png
from PIL import Image
import pyotp
import pathlib
The Account model has a one-to-one relationship with the Django built-in User model. This is where we keep track of the totp secret key and boolean value for having totp authentication enabled.
setUp TestCase
...
import pathlib
class TestEnableTOTPViewTestCase(TestCase):
def setUp(self):
user_a = User.objects.create(
username='user_a')
user_a.set_password('password_user_a')
user_a.save()
Account.objects.create(user=user_a)
We create a test user and save that in the test database.
GET Form
class TestEnableTOTPViewTestCase(TestCase):
...
def test_enable_totp_view(self):
self.client.login(
username='user_a',
password='password_user_a'
)
get_response = self.client.get(
reverse('accounts:enable_totp')
)
self.assertEquals(
get_response.status_code, 200
)
self.assertTemplateUsed(
get_response, 'accounts/totp_form.html'
)
self.assertEquals(
get_response.request['PATH_INFO'],
'/accounts/enable-totp/'
)
Consume SVG with BeautifulSoup
class TestEnableTOTPViewTestCase(TestCase):
...
def test_enable_totp_view(self):
...
soup = BeautifulSoup(
get_response.content, features="lxml"
)
svg_container = soup.find(
"div", {"id": "svgcontainer"}
)
self.assertIsNotNone(svg_container)
scsvg = svg_container.findChild(
"svg", recursive=False
)
self.assertIsNotNone(scsvg)
with open('qr.svg', 'w') as f:
x_string = "<?xml version='1.0'"
x_string += " encoding='utf-8'?>\n"
f.write(x_string + str(scsvg))
div
with an id
of svgcontainer. We capture the xml of the svg in the variable
scsvg
, and then write it out to disc.
svg2png
class TestEnableTOTPViewTestCase(TestCase):
...
def test_enable_totp_view(self):
...
svg2png(
url='qr.svg', write_to='qr.png',
scale=8
)
Add White Background
class TestEnableTOTPViewTestCase(TestCase):
...
def test_enable_totp_view(self):
...
t_image = Image.open('qr.png')
t_image.load()
background = Image.new(
"RGB", t_image.size,
(255, 255, 255)
)
background.paste(
t_image,
mask=t_image.split()[3]
)
background.save(
'qr.jpg', "JPEG",
quality=100
)
Extract Data From QRCODE
class TestEnableTOTPViewTestCase(TestCase):
...
def test_enable_totp_view(self):
...
image = cv2.imread('qr.jpg')
qr_det = cv2.QRCodeDetector()
qrdata = qr_det.detectAndDecode(image)
totp_code = pyotp.TOTP(
pyotp.parse_uri(qrdata[0]).secret
).now()
qrdata[0]
will be the otpauth_uri.pyotp.parse_uri(qrdata[0]).secret
is the secret key.totp_code
is the one-time password.
POST the totp_code
class TestEnableTOTPViewTestCase(TestCase):
...
def test_enable_totp_view(self):
...
totp_code = pyotp.TOTP(
pyotp.parse_uri(qrdata[0]).secret
).now()
response = self.client.post(
reverse('accounts:enable_totp'),
{'totp_code': totp_code},
follow=True
)
self.assertEquals(
response.status_code, 200
)
self.assertTemplateUsed(
response, 'base_form.html'
)
self.assertEquals(
response.request['PATH_INFO'],
'/accounts/edit-profile/'
)
user_a = User.objects.get(
username='user_a'
)
self.assertTrue(user_a.account.use_totp)
self.assertEquals(
len(user_a.account.totp_key), 16
)
pathlib.Path('qr.svg').unlink()
pathlib.Path('qr.png').unlink()
pathlib.Path('qr.jpg').unlink()
totp_code
back to the form, and then verify
that the database is updated, and then delete the image
files.
Complete TestCase
# tp/accounts/tests/test_enable_totp_view.py
# python manage.py test accounts.tests.test_enable_totp_view
from django.test import TestCase
from django.contrib.auth.models import User
from accounts.models import Account
from django.urls import reverse
from bs4 import BeautifulSoup
import cv2
from cairosvg import svg2png
from PIL import Image
import pyotp
class TestEnableTOTPViewTestCase(TestCase):
# setUP TestCase
def setUp(self):
user_a = User.objects.create(
username='user_a')
user_a.set_password('password_user_a')
user_a.save()
Account.objects.create(user=user_a)
def test_enable_totp_view(self):
self.client.login(
username='user_a',
password='password_user_a'
)
# GET Form
get_response = self.client.get(
reverse('accounts:enable_totp')
)
self.assertEquals(
get_response.status_code, 200
)
self.assertTemplateUsed(
get_response, 'accounts/totp_form.html'
)
self.assertEquals(
get_response.request['PATH_INFO'],
'/accounts/enable-totp/'
)
# Consume SVG with BeautifulSoup
soup = BeautifulSoup(
get_response.content, features="lxml"
)
svg_container = soup.find(
"div", {"id": "svgcontainer"}
)
self.assertIsNotNone(svg_container)
scsvg = svg_container.findChild(
"svg", recursive=False
)
self.assertIsNotNone(scsvg)
with open('qr.svg', 'w') as f:
x_string = "<?xml version='1.0'"
x_string += " encoding='utf-8'?>\n"
f.write(x_string + str(scsvg))
# svg2png
svg2png(
url='qr.svg', write_to='qr.png',
scale=8
)
# add white background
t_image = Image.open('qr.png')
t_image.load()
background = Image.new(
"RGB", t_image.size,
(255, 255, 255)
)
background.paste(
t_image,
mask=t_image.split()[3]
)
background.save(
'qr.jpg', "JPEG",
quality=100
)
# extract data from qrcode
image = cv2.imread('qr.jpg')
qr_det = cv2.QRCodeDetector()
qrdata = qr_det.detectAndDecode(image)
totp_code = pyotp.TOTP(
pyotp.parse_uri(qrdata[0]).secret
).now()
totp_code = pyotp.TOTP(
pyotp.parse_uri(qrdata[0]).secret
).now()
# POST the `totp_code`
response = self.client.post(
reverse('accounts:enable_totp'),
{'totp_code': totp_code},
follow=True
)
self.assertEquals(
response.status_code, 200
)
self.assertTemplateUsed(
response, 'base_form.html'
)
self.assertEquals(
response.request['PATH_INFO'],
'/accounts/edit-profile/'
)
user_a = User.objects.get(
username='user_a'
)
self.assertTrue(user_a.account.use_totp)
self.assertEquals(
len(user_a.account.totp_key), 16
)
pathlib.Path('qr.svg').unlink()
pathlib.Path('qr.png').unlink()
pathlib.Path('qr.jpg').unlink()