qrcode: add frontend qrcode reader (#82652)
gitea/passerelle/pipeline/head This commit looks good
Details
gitea/passerelle/pipeline/head This commit looks good
Details
This commit is contained in:
parent
7314fa224c
commit
32d3dd01bc
|
@ -12,6 +12,7 @@ passerelle.egg-info/
|
|||
coverage.xml
|
||||
junit-py*.xml
|
||||
.sass-cache/
|
||||
passerelle/static/css/style.css
|
||||
passerelle/static/css/style.css.map
|
||||
passerelle/**/static/**/css/style.css
|
||||
passerelle/**/static/**/css/style.css.map
|
||||
node_modules/
|
||||
coverage/
|
||||
|
|
10
README
10
README
|
@ -126,3 +126,13 @@ django-jsonresponse (https://github.com/jjay/django-jsonresponse)
|
|||
# Files: passerelle/utils/jsonresponse.py
|
||||
# Copyright (c) 2012 Yasha Borevich <j.borevich@gmail.com>
|
||||
# Licensed under the BSD license
|
||||
|
||||
tweetnacl-js (https://github.com/dchest/tweetnacl-js)
|
||||
# Files: passerelle/apps/qrcode/static/qrcode/js/nacl.min.js
|
||||
# Copyright: https://github.com/dchest/tweetnacl-js/blob/master/AUTHORS.md
|
||||
# Licensed under the Unlicense license (public domain)
|
||||
|
||||
zxing-browser (https://github.com/zxing-js/browser/)
|
||||
# Files: passerelle/apps/qrcode/static/qrcode/js/zxing-browser.min.js
|
||||
# Copyright: (c) 2018 ZXing for JS
|
||||
# Licensed under the MIT license.
|
||||
|
|
|
@ -67,6 +67,16 @@ class QRCodeConnector(BaseResource):
|
|||
class Meta:
|
||||
verbose_name = _('QR Code')
|
||||
|
||||
@property
|
||||
def signing_key(self):
|
||||
binary_key = binascii.unhexlify(self.key)
|
||||
return SigningKey(seed=binary_key)
|
||||
|
||||
@property
|
||||
def hex_verify_key(self):
|
||||
verify_key = self.signing_key.verify_key.encode()
|
||||
return binascii.hexlify(verify_key).decode('utf-8')
|
||||
|
||||
@endpoint(
|
||||
name='save-certificate',
|
||||
pattern=f'^{UUID_PATTERN}?$',
|
||||
|
@ -208,6 +218,7 @@ class QRCodeConnector(BaseResource):
|
|||
|
||||
@endpoint(
|
||||
name='open-reader',
|
||||
perm='OPEN',
|
||||
description=_('Open a QRCode reader page.'),
|
||||
pattern=f'^{UUID_PATTERN}$',
|
||||
example_pattern='{uuid}',
|
||||
|
@ -227,6 +238,7 @@ class QRCodeConnector(BaseResource):
|
|||
context={
|
||||
'started': now >= reader.validity_start,
|
||||
'expired': now >= reader.validity_end,
|
||||
'verify_key': self.hex_verify_key,
|
||||
'reader': reader,
|
||||
},
|
||||
)
|
||||
|
@ -272,9 +284,7 @@ class Certificate(models.Model):
|
|||
'validity_end': str(self.validity_end.timestamp()),
|
||||
} | self.data
|
||||
msg = encode_mime_like(data)
|
||||
binary_key = binascii.unhexlify(self.resource.key)
|
||||
signing_key = SigningKey(seed=binary_key)
|
||||
signed = signing_key.sign(msg)
|
||||
signed = self.resource.signing_key.sign(msg)
|
||||
qr_code = QRCode(image_factory=PilImage, error_correction=ERROR_CORRECT_Q)
|
||||
qr_code.add_data(b45encode(signed).decode())
|
||||
qr_code.make(fit=True)
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
$red: #C4381C;
|
||||
$red-light: #FCECE8;
|
||||
$green: #47752F;
|
||||
$green-light: #F2F7EE;
|
||||
$gray-light: #CECECE;
|
||||
|
||||
qrcode-reader {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.qrcode-reader {
|
||||
|
||||
&--video-wrapper {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
display: grid;
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
&--video {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
|
||||
&--fullscreen-button {
|
||||
z-index: 1;
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
align-self: end;
|
||||
justify-self: end;
|
||||
|
||||
border: none;
|
||||
fill: white;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
&--wrapper.fullscreen &--enter-fullscreen-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--exit-fullscreen-icon {
|
||||
display: none;
|
||||
|
||||
}
|
||||
|
||||
&--wrapper.fullscreen &--exit-fullscreen-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&--popup {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
align-self: end;
|
||||
justify-self: center;
|
||||
|
||||
--title-background: #{$green-light};
|
||||
--title-color: #{$green};
|
||||
|
||||
border: 2px solid var(--title-color);
|
||||
border-radius: 5px;
|
||||
margin: 10px;
|
||||
box-shadow: 0 0 10px 3px #ffffff;
|
||||
background: white;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-items: center;
|
||||
|
||||
&.error {
|
||||
--title-background: #{$red-light};
|
||||
--title-color: #{$red};
|
||||
}
|
||||
|
||||
&.closed {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--popup-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border-top-right-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
padding: 5px;
|
||||
color: var(--title-color);
|
||||
background-color: var(--title-background);
|
||||
}
|
||||
|
||||
&--popup-content {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
&--validity {
|
||||
color: var(--title-color);
|
||||
font-weight: bold;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&--validity-label {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
&--data-items {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
margin-top: 5px;
|
||||
padding-top: 5px;
|
||||
border-top: 1px solid var(--gray-light);
|
||||
}
|
||||
|
||||
&--data-item-label {
|
||||
font-weight: bold;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
&--data-item-value {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&--close-popup-button {
|
||||
margin: 5px 10px 10px 10px;
|
||||
padding: 5px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,269 @@
|
|||
import './nacl.min.js'
|
||||
import './zxing-browser.min.js'
|
||||
|
||||
const translations = (() => {
|
||||
const i18nElement = window.document.getElementById('qrcode-reader-i18n')
|
||||
if (i18nElement) {
|
||||
return JSON.parse(i18nElement.innerHTML)
|
||||
}
|
||||
return {}
|
||||
})()
|
||||
|
||||
function translate (key) { return translations[key] || key }
|
||||
|
||||
function template (innerHTML) {
|
||||
const templateElement = document.createElement('template')
|
||||
templateElement.innerHTML = innerHTML
|
||||
return templateElement
|
||||
}
|
||||
|
||||
const notSupportedTemplate = template(`<p>${translate('not_supported')}</p>`)
|
||||
|
||||
const readerTemplate = template(`
|
||||
<div class="qrcode-reader--video-wrapper">
|
||||
<video class="qrcode-reader--video"></video>
|
||||
<div class="qrcode-reader--fullscreen-button">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="qrcode-reader--enter-fullscreen-icon">
|
||||
<path d="M8 3V5H4V9H2V3H8ZM2 21V15H4V19H8V21H2ZM22 21H16V19H20V15H22V21ZM22 9H20V5H16V3H22V9Z"></path>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="qrcode-reader--exit-fullscreen-icon">
|
||||
<path d="M18 7H22V9H16V3H18V7ZM8 9H2V7H6V3H8V9ZM18 17V21H16V15H22V17H18ZM8 15V21H6V17H2V15H8Z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="qrcode-reader--popup closed">
|
||||
<div class="qrcode-reader--popup-title"></div>
|
||||
<div class="qrcode-reader--popup-content"></div>
|
||||
<button class="qrcode-reader--close-popup-button">${translate('close')}</button>
|
||||
</div>
|
||||
`)
|
||||
|
||||
const validityTemplate = template(`
|
||||
<div class="qrcode-reader--validity">
|
||||
<div class="qrcode-reader--validity-label">${translate('from')} :</div>
|
||||
<div>{validityStart}</div>
|
||||
<div>${translate('to')} :</div>
|
||||
<div>{validityEnd}</div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
const dataTemplate = template(`
|
||||
<div class="qrcode-reader--data-items"></div>
|
||||
`)
|
||||
|
||||
const dataItemTemplate = template(`
|
||||
<span class="qrcode-reader--data-item-label">{label} : </span>
|
||||
<span class="qrcode-reader--data-item-value">{value}</span>
|
||||
`)
|
||||
|
||||
function decodeMimeLike (value) {
|
||||
const chunks = value.split('\n')
|
||||
const data = {}
|
||||
let k = null
|
||||
let v = null
|
||||
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const line = chunks[i]
|
||||
if (line.startsWith(' ')) {
|
||||
if (k !== null) {
|
||||
v += '\n' + line.slice(1)
|
||||
}
|
||||
} else {
|
||||
if (k !== null) {
|
||||
data[k] = v
|
||||
k = null
|
||||
v = null
|
||||
}
|
||||
if (line.indexOf(': ') !== -1) {
|
||||
const parts = line.split(': ', 2)
|
||||
k = parts[0]
|
||||
v = parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (k !== null) {
|
||||
data[k] = v
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function divmod (a, b) {
|
||||
let remainder = a
|
||||
let quotient = 0
|
||||
if (a >= b) {
|
||||
remainder = a % b
|
||||
quotient = (a - remainder) / b
|
||||
}
|
||||
return [quotient, remainder]
|
||||
}
|
||||
|
||||
const BASE45_CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
|
||||
|
||||
function decodeBase45 (str) {
|
||||
const output = []
|
||||
const buf = []
|
||||
|
||||
for (let i = 0, length = str.length; i < length; i++) {
|
||||
const j = BASE45_CHARSET.indexOf(str[i])
|
||||
if (j < 0) { throw new Error('Base45 decode: unknown character') }
|
||||
buf.push(j)
|
||||
}
|
||||
|
||||
for (let i = 0, length = buf.length; i < length; i += 3) {
|
||||
const x = buf[i] + buf[i + 1] * 45
|
||||
if (length - i >= 3) {
|
||||
const [d, c] = divmod(x + buf[i + 2] * 45 * 45, 256)
|
||||
output.push(d)
|
||||
output.push(c)
|
||||
} else {
|
||||
output.push(x)
|
||||
}
|
||||
}
|
||||
return new Uint8Array(output)
|
||||
}
|
||||
|
||||
class QRCodeReader extends window.HTMLElement {
|
||||
#popup
|
||||
#popupContent
|
||||
#popupTitle
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
if (!this.#supported()) {
|
||||
this.appendChild(notSupportedTemplate.content.cloneNode(true))
|
||||
return
|
||||
}
|
||||
|
||||
this.appendChild(readerTemplate.content.cloneNode(true))
|
||||
|
||||
this.#popup = this.querySelector('.qrcode-reader--popup')
|
||||
this.#popupContent = this.querySelector('.qrcode-reader--popup-content')
|
||||
this.#popupTitle = this.querySelector('.qrcode-reader--popup-title')
|
||||
|
||||
const closePopupButton = this.querySelector('.qrcode-reader--close-popup-button')
|
||||
closePopupButton.addEventListener('click', () => {
|
||||
this.#popup.classList.add('closed')
|
||||
})
|
||||
|
||||
const fullScreenButton = this.querySelector('.qrcode-reader--fullscreen-button')
|
||||
fullScreenButton.addEventListener('click', () => {
|
||||
this.#toggleFullScreen()
|
||||
})
|
||||
}
|
||||
|
||||
connectedCallback () {
|
||||
if (!this.#supported()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.#startScan()
|
||||
}
|
||||
|
||||
async #startScan () {
|
||||
const codeReader = new window.ZXingBrowser.BrowserQRCodeReader()
|
||||
const videoElement = this.querySelector('.qrcode-reader--video')
|
||||
await codeReader.decodeFromVideoDevice(undefined, videoElement, (result) => {
|
||||
if (result) {
|
||||
this.#showResult(result.text)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get #verifyKey () {
|
||||
const hexKey = this.getAttribute('verify-key')
|
||||
return new Uint8Array(hexKey.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16)))
|
||||
}
|
||||
|
||||
#supported () {
|
||||
return !!navigator.mediaDevices
|
||||
}
|
||||
|
||||
#showResult (qrCodeContent) {
|
||||
this.#popup.classList.remove('error')
|
||||
this.#popup.classList.remove('closed')
|
||||
|
||||
let signed
|
||||
try {
|
||||
signed = decodeBase45(qrCodeContent)
|
||||
} catch (error) {
|
||||
this.#showError(translate('invalid_qrcode'))
|
||||
return
|
||||
}
|
||||
|
||||
const opened = window.nacl.sign.open(signed, this.#verifyKey)
|
||||
if (opened == null) {
|
||||
this.#showError(translate('invalid_signature'))
|
||||
return
|
||||
}
|
||||
|
||||
this.#popupContent.innerHTML = ''
|
||||
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
const decoded = decoder.decode(opened)
|
||||
const data = decodeMimeLike(decoded)
|
||||
|
||||
delete data.uuid
|
||||
|
||||
const validityStart = new Date(parseFloat(data.validity_start) * 1000)
|
||||
delete data.validity_start
|
||||
|
||||
const validityEnd = new Date(parseFloat(data.validity_end) * 1000)
|
||||
delete data.validity_end
|
||||
|
||||
const now = new Date()
|
||||
|
||||
if (now.getTime() < validityStart.getTime()) {
|
||||
this.#popupTitle.innerText = translate('not_yet_valid')
|
||||
this.#popup.classList.add('error')
|
||||
} else if (now.getTime() > validityEnd.getTime()) {
|
||||
this.#popupTitle.innerText = translate('expired')
|
||||
this.#popup.classList.add('error')
|
||||
} else {
|
||||
this.#popupTitle.innerText = translate('valid')
|
||||
}
|
||||
|
||||
const validityElement = validityTemplate.cloneNode(true)
|
||||
validityElement.innerHTML = validityElement.innerHTML
|
||||
.replace('{validityStart}', validityStart.toLocaleString())
|
||||
.replace('{validityEnd}', validityEnd.toLocaleString())
|
||||
|
||||
this.#popupContent.append(validityElement.content)
|
||||
|
||||
const dataElement = dataTemplate.cloneNode(true)
|
||||
|
||||
const dataItems = dataElement.content.querySelector('.qrcode-reader--data-items')
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const dataItem = dataItemTemplate.cloneNode(true)
|
||||
dataItem.innerHTML = dataItem.innerHTML.replace('{label}', key).replace('{value}', value)
|
||||
dataItems.append(dataItem.content)
|
||||
}
|
||||
|
||||
this.#popupContent.append(dataElement.content)
|
||||
}
|
||||
|
||||
#showError (message) {
|
||||
this.#popup.classList.remove('closed')
|
||||
this.#popup.classList.add('error')
|
||||
this.#popupTitle.innerText = translate('invalid_title')
|
||||
this.#popupContent.innerText = message
|
||||
}
|
||||
|
||||
#toggleFullScreen () {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen()
|
||||
this.classList.remove('fullscreen')
|
||||
} else {
|
||||
this.requestFullscreen()
|
||||
this.classList.add('fullscreen')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('qrcode-reader', QRCodeReader)
|
File diff suppressed because it is too large
Load Diff
|
@ -1,20 +1,36 @@
|
|||
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script type="application/json" id="qrcode-reader-i18n">
|
||||
{
|
||||
"close": "{% trans 'Close' %}",
|
||||
"expired": "{% trans 'QR code Expired' %}",
|
||||
"from": "{% trans 'From' %}",
|
||||
"invalid_content": "{% trans 'This QR code isn\'t supported by this application.' %}",
|
||||
"invalid_signature": "{% trans 'Signature verification failed.' %}",
|
||||
"invalid_title": "Invalid QR Code",
|
||||
"not_supported": "{% trans 'QR code reader isn\'t supported on your platform. Please update your browser.' %}",
|
||||
"not_yet_valid": "{% trans 'QR code not yet valid' %}",
|
||||
"to": "{% trans 'To' %}",
|
||||
"valid": "{% trans 'Valid QR code' %}"
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="{% static 'qrcode/css/style.css' %}">
|
||||
<script type="module" src="{% static 'qrcode/js/qrcode-reader.js' %}"></script>
|
||||
</head>
|
||||
<style>
|
||||
</style>
|
||||
<body>
|
||||
{% if not started %}
|
||||
{% trans "Reader isn't usable yet." %}
|
||||
{% elif expired %}
|
||||
{% trans "Reader has expired." %}
|
||||
{% else %}
|
||||
{{ reader.validity_start }}<br />
|
||||
{{ now }}
|
||||
{{ reader.uuid }}
|
||||
<qrcode-reader verify-key="{{ verify_key }}">
|
||||
</qrcode-reader>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,6 +1,205 @@
|
|||
import { expect, test} from 'vitest'
|
||||
import 'qrcode/qrcode-reader.js'
|
||||
import { expect, test, vi} from 'vitest'
|
||||
import ZXingBrowser from 'qrcode/zxing-browser.min.js'
|
||||
import nacl from 'qrcode/nacl.min.js'
|
||||
|
||||
test('test qrcode', async () => {
|
||||
expect(true).toBe(true)
|
||||
const okCodeData =
|
||||
'SVF.WB899RLB0%7NFKM.IWSBLTVZ65OQKD59REJ+ I/IDL960%HL%F 5EVVK397HQGIUK3OAPOP0RF/X' +
|
||||
'L1FIKJG08J5 LE.G9%EPEDUF7M.C%47SW64DC4W5NF6$CC1S64VCBW53ECQZCGPCSG6C%6YZC:DC-96N' +
|
||||
'E1AECPED-EDLFF QEGEC9VE634L%6 47%47C%6446D36K/EXVDAVCRWEV2C0/DUF7:967461R67:6OA7' +
|
||||
'Y$5DE1HEC1WE..DF$DUF7/B8-ED:JCSTDR.C4LE1WE..DF$DUF771904E93DKOEXVDKPCF/DV CP9EIE' +
|
||||
'C*ZCT34ZA8.Q6$Q6HB8A0'
|
||||
|
||||
const invalidCodeData = 'https://georges-abitbol.fr'
|
||||
|
||||
const invalidSignatureData =
|
||||
'ELT7PRQKJ097ZM1TA5$EJC%NOMVL5D5QNEZUR.Q5JESA5D+GO*I16IAYJO:D4ZGU GTVIM%DHI88%CGD' +
|
||||
'0W6LBKOWSKLRC$ R9%EPEDUF7OB7GPC 57Y47BX5-A6+/61S6$57CW5/Q6+TCRW6GA7 57QF6H*6C56N' +
|
||||
'E1AECPED-EDLFF QEGEC9VE634L%6 47%47C%6446D36K/EXVDAVCRWEV2C0/DUF7:96646L%6946OA7' +
|
||||
'Y$5DE1HEC1WE..DF$DUF70A8R.C4LE1WE..DF$DUF7VF8XVDKPCF/DV CP9EIEC*ZCO34A0'
|
||||
|
||||
|
||||
const qrcodeReaderTest = test.extend({
|
||||
mock: async ({ task }, use) => {
|
||||
let resolveResult = undefined
|
||||
let resultPromise = new Promise((resolve) => resolveResult = resolve)
|
||||
let resolveResultHandled = undefined
|
||||
const terminate = Symbol()
|
||||
|
||||
class MockBrowserQRCodeReader {
|
||||
async decodeFromVideoDevice(device, element, callback) {
|
||||
while(true) {
|
||||
const result = await resultPromise
|
||||
|
||||
if(result === terminate) {
|
||||
return
|
||||
}
|
||||
|
||||
resultPromise = new Promise((resolve) => resolveResult = resolve)
|
||||
|
||||
callback({ text: result })
|
||||
|
||||
resolveResultHandled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.nacl = nacl
|
||||
navigator.mediaDevices = true
|
||||
const savedZXingBrowser = window.ZXingBrowser
|
||||
window.ZXingBrowser = { BrowserQRCodeReader : MockBrowserQRCodeReader }
|
||||
|
||||
const reader = document.createElement('qrcode-reader')
|
||||
|
||||
reader.setAttribute('verify-key', 'f81af42f9f9422d2393859d40994a42cdb2ef68507f056292ac96d1de1f1af83')
|
||||
document.append(reader)
|
||||
|
||||
await use({
|
||||
reader,
|
||||
scan: async (text) => {
|
||||
const resultHandled = new Promise((resolve) => resolveResultHandled = resolve)
|
||||
resolveResult(text)
|
||||
await resultHandled
|
||||
}
|
||||
})
|
||||
vi.useFakeTimers()
|
||||
|
||||
reader.remove()
|
||||
|
||||
vi.useRealTimers ()
|
||||
|
||||
resolveResult(terminate)
|
||||
window.ZXingBrowser = savedZXingBrowser
|
||||
navigator.mediaDevices = undefined
|
||||
window.nacl = undefined
|
||||
}
|
||||
})
|
||||
|
||||
test('qrcode reader shows a warning message if not supported on platform', async ({mock}) => {
|
||||
const reader = document.createElement('qrcode-reader')
|
||||
reader.setAttribute('verify-key', 'f81af42f9f9422d2393859d40994a42cdb2ef68507f056292ac96d1de1f1af83')
|
||||
document.append(reader)
|
||||
|
||||
expect(reader.innerText).toBe('not_supported')
|
||||
})
|
||||
|
||||
qrcodeReaderTest('qrcode reader shows valid qrcode informations', async ({mock}) => {
|
||||
const { reader, scan } = mock
|
||||
|
||||
vi.setSystemTime(new Date(2023, 11, 1))
|
||||
await scan(okCodeData)
|
||||
|
||||
const popup = reader.querySelector('.qrcode-reader--popup')
|
||||
const title = popup.querySelector('.qrcode-reader--popup-title')
|
||||
|
||||
expect(popup.classList.contains('closed')).toBe(false)
|
||||
expect(popup.classList.contains('error')).toBe(false)
|
||||
expect(title.innerText).toBe('valid')
|
||||
|
||||
const validity = popup.querySelector('.qrcode-reader--validity')
|
||||
|
||||
expect(validity.innerText).toMatch(/from :\s*31\/10\/2023 23:00:00\s*to :\s*01\/12\/2023 22:59:59/)
|
||||
|
||||
const labels = popup.querySelectorAll('.qrcode-reader--data-item-label')
|
||||
|
||||
expect(labels.length).toBe(3)
|
||||
expect(labels[0].innerText).toMatch('last_name')
|
||||
expect(labels[1].innerText).toMatch('first_name')
|
||||
expect(labels[2].innerText).toMatch('license_plate')
|
||||
|
||||
const values = popup.querySelectorAll('.qrcode-reader--data-item-value')
|
||||
|
||||
expect(values.length).toBe(3)
|
||||
expect(values[0].innerText).toMatch('Abitbol')
|
||||
expect(values[1].innerText).toMatch('Georges')
|
||||
expect(values[2].innerText).toMatch('HA-424-AH')
|
||||
|
||||
const closeButton = reader.querySelector('.qrcode-reader--close-popup-button')
|
||||
closeButton.dispatchEvent(new Event('click'))
|
||||
|
||||
expect(popup.classList.contains('closed')).toBe(true)
|
||||
})
|
||||
|
||||
qrcodeReaderTest('qrcode reader shows error on not yet valid or expired qrcodes', async ({mock}) => {
|
||||
const { reader, scan } = mock
|
||||
vi.setSystemTime(new Date(2023, 9, 31)) // monthes start at 0 index, wtf javascript
|
||||
|
||||
await scan(okCodeData)
|
||||
|
||||
const popup = reader.querySelector('.qrcode-reader--popup')
|
||||
const title = popup.querySelector('.qrcode-reader--popup-title')
|
||||
|
||||
expect(popup.classList.contains('closed')).toBe(false)
|
||||
expect(popup.classList.contains('error')).toBe(true)
|
||||
expect(title.innerText).toBe('not_yet_valid')
|
||||
|
||||
vi.setSystemTime(new Date(2023, 11, 2)) // monthes start at 0 index, wtf javascript
|
||||
await scan(okCodeData)
|
||||
|
||||
expect(popup.classList.contains('closed')).toBe(false)
|
||||
expect(popup.classList.contains('error')).toBe(true)
|
||||
expect(title.innerText).toBe('expired')
|
||||
})
|
||||
|
||||
qrcodeReaderTest('qrcode reader shows error on invalid qrcode', async ({mock}) => {
|
||||
const { reader, scan } = mock
|
||||
|
||||
await scan(invalidCodeData)
|
||||
|
||||
const popup = reader.querySelector('.qrcode-reader--popup')
|
||||
const title = reader.querySelector('.qrcode-reader--popup-title')
|
||||
const content = reader.querySelector('.qrcode-reader--popup-content')
|
||||
|
||||
expect(popup.classList.contains('closed')).toBe(false)
|
||||
expect(popup.classList.contains('error')).toBe(true)
|
||||
expect(title.innerText).toBe('invalid_title')
|
||||
expect(content.innerText).toBe('invalid_qrcode')
|
||||
|
||||
const closeButton = reader.querySelector('.qrcode-reader--close-popup-button')
|
||||
closeButton.dispatchEvent(new Event('click'))
|
||||
|
||||
expect(popup.classList.contains('closed')).toBe(true)
|
||||
})
|
||||
|
||||
qrcodeReaderTest('qrcode reader shows error on invalid signature', async ({mock}) => {
|
||||
const { reader, scan } = mock
|
||||
|
||||
await scan(invalidSignatureData)
|
||||
|
||||
const popup = reader.querySelector('.qrcode-reader--popup')
|
||||
const title = reader.querySelector('.qrcode-reader--popup-title')
|
||||
const content = reader.querySelector('.qrcode-reader--popup-content')
|
||||
|
||||
expect(popup.classList.contains('closed')).toBe(false)
|
||||
expect(popup.classList.contains('error')).toBe(true)
|
||||
expect(title.innerText).toBe('invalid_title')
|
||||
expect(content.innerText).toBe('invalid_signature')
|
||||
|
||||
const closeButton = reader.querySelector('.qrcode-reader--close-popup-button')
|
||||
closeButton.dispatchEvent(new Event('click'))
|
||||
|
||||
expect(popup.classList.contains('closed')).toBe(true)
|
||||
})
|
||||
|
||||
qrcodeReaderTest('qrcode reader can toggle fullscreen', async ({mock}) => {
|
||||
const { reader } = mock
|
||||
const fullscreenButton = reader.querySelector('.qrcode-reader--fullscreen-button')
|
||||
|
||||
reader.requestFullscreen = vi.fn()
|
||||
document.exitFullscreen = vi.fn()
|
||||
|
||||
fullscreenButton.dispatchEvent(new Event('click'))
|
||||
|
||||
expect(reader.requestFullscreen).toHaveBeenCalled()
|
||||
expect(document.exitFullscreen).not.toHaveBeenCalled()
|
||||
expect(reader.classList.contains('fullscreen')).toBe(true)
|
||||
|
||||
vi.clearAllMocks()
|
||||
document.fullscreenElement = reader
|
||||
|
||||
fullscreenButton.dispatchEvent(new Event('click'))
|
||||
|
||||
expect(reader.requestFullscreen).not.toHaveBeenCalled()
|
||||
expect(document.exitFullscreen).toHaveBeenCalled()
|
||||
expect(reader.classList.contains('fullscreen')).toBe(false)
|
||||
})
|
||||
|
|
|
@ -190,7 +190,7 @@ def test_open_reader(app, connector, freezer):
|
|||
freezer.move_to('2022-01-01T10:00:00')
|
||||
result = app.get(f'{endpoint}/{reader.uuid}')
|
||||
|
||||
assert str(reader.uuid) in result.body.decode('utf-8')
|
||||
assert result.pyquery(f'qrcode-reader[verify-key="{connector.hex_verify_key}"]')
|
||||
|
||||
freezer.move_to('2023-01-01T10:00:01')
|
||||
result = app.get(f'{endpoint}/{reader.uuid}')
|
||||
|
|
Loading…
Reference in New Issue