# Test QRCODE Svg in Django - Trent's Blog
Trent's blog of mostly technical documentations.

Skip to content

# 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.

enable totp confirmation form

## 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/'
        )

The TestCase requires two requests. In the first request we GET the form so that we can consume the qrcode.

## 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))

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

    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

    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

    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()

Post the `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()