add docs/posts/test-qr-svg-django.md
This commit is contained in:
parent
0f885a69fd
commit
f4b6a3a26a
@ -5,6 +5,7 @@ authors: ["trent"]
|
||||
# Trent's Blog
|
||||
## **Posts By Date**
|
||||
|
||||
* [2021-04-19: Test QRCODE Svg in Django](posts/test-qr-svg-django.md){target=_blank}
|
||||
* [2021-01-25: Prosody Photo Uploads](posts/prosody-photo-uploads.md){target=_blank}
|
||||
* [2021-01-09: Xmpp Apt Notifications](posts/xmpp-apt-notifications.md){target=_blank}
|
||||
* [2020-12-20: Apache Virtual Hosts](posts/apache-virtual-hosts.md){target=_blank}
|
||||
|
@ -7,6 +7,7 @@ authors: ["trent"]
|
||||
## **Links**
|
||||
|
||||
* [Home](index.md){target=_blank}
|
||||
* [AudioBooks](https://trentpalmer.org){target=_blank}
|
||||
* [GitHub](https://github.com/TrentSPalmer){target=_blank}
|
||||
* [Twitter](https://twitter.com/boringtrent){target=_blank}
|
||||
* [Facebook](https://www.facebook.com/trentspalmer){target=_blank}
|
||||
|
BIN
docs/photos/IMG_screenshot_enable_totp_confirmation_form.jpg
Normal file
BIN
docs/photos/IMG_screenshot_enable_totp_confirmation_form.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
368
docs/posts/test-qr-svg-django.md
Normal file
368
docs/posts/test-qr-svg-django.md
Normal file
@ -0,0 +1,368 @@
|
||||
---
|
||||
title: "Test QRCODE Svg in Django"
|
||||
date: 2021-04-19
|
||||
draft: false
|
||||
tags: ["django", "python", "testing", "pillow", "beautifulsoup", "opencv", "cairosvg", "pyotp"]
|
||||
authors: ["trent"]
|
||||
---
|
||||
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](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics){target=_blank},
|
||||
scalable vector graphics, it is an image that is
|
||||
completely rendered from a
|
||||
[string](https://en.wikipedia.org/wiki/String_(computer_science)){target=_blank}
|
||||
of text, or more accurately [XML](https://en.wikipedia.org/wiki/XML){target=_blank}
|
||||
text, unlike [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics){target=_blank}
|
||||
or [JPG](https://en.wikipedia.org/wiki/JPEG){target=_blank}, which are
|
||||
[binary file](https://en.wikipedia.org/wiki/Binary_file){target=_blank}s.
|
||||
|
||||
### Python Libraries Used
|
||||
* [BeautifulSoup](https://pypi.org/project/beautifulsoup4/){target=_blank}
|
||||
to consume html
|
||||
* [CairoSVG](https://pypi.org/project/CairoSVG/){target=_blank}
|
||||
to convert svg to png
|
||||
* [python-pillow](https://pypi.org/project/Pillow/){target=_blank}
|
||||
to convert transparent png background to white
|
||||
* [opencv-python](https://pypi.org/project/opencv-python/){target=_blank}
|
||||
to extract data from qrcode
|
||||
* [python-pyotp](https://pypi.org/project/pyotp/){target=_blank}
|
||||
|
||||
## **Form ScreenShot**
|
||||
This is the form we will be testing.
|
||||
|
||||
In real life you would confirm that you want to use
|
||||
[two-factor-authentication](https://en.wikipedia.org/wiki/Multi-factor_authentication){target=_blank}
|
||||
by scanning the qrcode with an
|
||||
[authentication app](https://play.google.com/store/apps/details?id=org.shadowice.flocke.andotp){target=_blank}
|
||||
on your smartphone, and then enter the
|
||||
[time-based one-time password](https://en.wikipedia.org/wiki/Time-based_One-Time_Password){target=_blank}.
|
||||
|
||||
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](https://pypi.org/project/pyotp/){target=_blank}, after extracting
|
||||
the secret key from the qrcode.
|
||||
|
||||
<div align="center">
|
||||
<em>enable totp confirmation form</em>
|
||||
</div>
|
||||
|
||||
!["ScreenShot of a confirmation form for enabling totp (in a django app)"](
|
||||
../../photos/IMG_screenshot_enable_totp_confirmation_form.jpg){: .center }
|
||||
|
||||
## **Import Python Libraries**
|
||||
```python
|
||||
# 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**
|
||||
```python
|
||||
...
|
||||
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**
|
||||
```python
|
||||
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/'
|
||||
)
|
||||
```
|
||||
The TestCase requires two requests. In the first request we GET the form
|
||||
so that we can consume the qrcode.
|
||||
|
||||
## **Consume SVG with BeautifulSoup**
|
||||
```python
|
||||
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))
|
||||
```
|
||||
The inline xml for the svg comes to us as a child of a
|
||||
`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**
|
||||
```python
|
||||
class TestEnableTOTPViewTestCase(TestCase):
|
||||
...
|
||||
def test_enable_totp_view(self):
|
||||
...
|
||||
svg2png(
|
||||
url='qr.svg', write_to='qr.png',
|
||||
scale=8
|
||||
)
|
||||
```
|
||||
With svg2png from CairoSVG, we convert the svg to png format. Opencv seems
|
||||
unable to consume the qrcode unless you scale it up.
|
||||
|
||||
## **Add White Background**
|
||||
```python
|
||||
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
|
||||
)
|
||||
```
|
||||
We use Image from python-pillow to change the background from transparent
|
||||
to white. Opencv seems unable to consume the qrcode when it has a transparent
|
||||
background.
|
||||
|
||||
## **Extract Data From QRCODE**
|
||||
```python
|
||||
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`**
|
||||
```python
|
||||
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()
|
||||
```
|
||||
Post the `totp_code` back to the form, and then verify
|
||||
that the database is updated, and then delete the image
|
||||
files.
|
||||
|
||||
## **Complete TestCase**
|
||||
```python
|
||||
# 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()
|
||||
```
|
@ -19,6 +19,7 @@ markdown_extensions:
|
||||
nav:
|
||||
- Home:
|
||||
- Home: index.md
|
||||
- posts/test-qr-svg-django.md
|
||||
- posts/prosody-photo-uploads.md
|
||||
- posts/xmpp-apt-notifications.md
|
||||
- posts/apache-virtual-hosts.md
|
||||
|
Loading…
Reference in New Issue
Block a user