passerelle/tests/js/qrcode.test.js

531 lines
18 KiB
JavaScript

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'
// private-key: 98f986e1afe2b546d264e45f00eb3f8d1b331bf3d2fa9e73bea3d8b2c9d90274
//
const okCodeData =
'3%73QJ0UK5G45S4XE0+GEUIKU662$D$K0RTM$4R7L7UX0V19FMFR5A++S6BNFR26 IJ7V15NE1NV2RN+T' +
'VH6%PHBDL*:01/69%EPEDUF7Y47EM6JA7MA79W5DOC$CC4S6G-CBW5C%6$Q6J*6JOCIPC4DC646FM6NE1' +
'AECPED-EDLFF QEGEC9VE634L%6 47%47C%6446D36K/EXVDAVCRWEV2C0/DUF7:967461R67:6OA7Y$5' +
'DE1HEC1WE..DF$DUF7/B8-ED:JCSTDR.C4LE1WE..DF$DUF771904E93DKOEXVDKPCF/DV CP9EIEC*ZC' +
'T34ZA8.Q6$Q6HB8A0'
const invalidCodeData = 'https://georges-abitbol.fr'
const invalidSignatureData =
'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 certificateWithoutValidity =
':U8$JSK1IVMJP$E06JCBCVQSIXFZ$HEDJ+S3:%1YN13BHJ$GFGLIN88AL5UNSFG8UG+YUV 2J6701809' +
'B3X3PP0TH9CN2R3IYEDB$DKWEOED0%EIEC ED3.DQ34%R83:6NW6XM8ME1.$E8UCE44QIC+96M.C9%6I' +
'M6E467W5LA76L6%47G%64W5ZJCYX6J*64EC6:6J$6'
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')
// private-key: 98f986e1afe2b546d264e45f00eb3f8d1b331bf3d2fa9e73bea3d8b2c9d90274
reader.setAttribute('verify-key', '15dbdd38a2d8a2db1b4dd985da8da2b4e6b785b28db3fe0b34a10cfb3ba0aeb3')
document.append(reader)
const fetchBackup = global.fetch
global.fetch = vi.fn()
vi.useFakeTimers({toFake: ['Date']})
await use({
reader,
scan: async (text) => {
const resultHandled = new Promise((resolve) => resolveResultHandled = resolve)
resolveResult(text)
await resultHandled
}
})
vi.useRealTimers()
global.fetch = fetchBackup
reader.remove()
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*10\/31\/2023, 11:00:00 PM\s*to :\s*12\/1\/2023, 10:59:59 PM/)
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')
const closeButton = reader.querySelector('.qrcode-reader--close-popup-button')
expect(popup.classList.contains('closed')).toBe(false)
expect(popup.classList.contains('error')).toBe(true)
expect(title.innerText).toBe('not_yet_valid')
closeButton.dispatchEvent(new Event('click'))
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()
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)
document.fullscreenElement = reader
reader.dispatchEvent(new Event('fullscreenchange'))
expect(reader.classList.contains('fullscreen')).toBe(true)
document.fullscreenElement = undefined
reader.dispatchEvent(new Event('fullscreenchange'))
expect(reader.classList.contains('fullscreen')).toBe(false)
})
qrcodeReaderTest('qrcode reader accepts certificate without validity dates', async ({mock}) => {
const { reader, scan } = mock
await scan(certificateWithoutValidity)
const popup = reader.querySelector('.qrcode-reader--popup')
const title = popup.querySelector('.qrcode-reader--popup-title')
const validity = popup.querySelector('.qrcode-reader--validity')
expect(popup.classList.contains('closed')).toBe(false)
expect(popup.classList.contains('error')).toBe(false)
expect(title.innerText).toBe('valid')
expect(validity.innerText.trim().split(/\s+/)).toStrictEqual(["from", ":", "always", "to", ":", "never"])
})
const qrcodeReaderMetadataTest = qrcodeReaderTest.extend({
loadMetadata: async ({ task, mock }, use) => {
const loadMetadata = async (qrCodeData, mockFetch) => {
const { reader, scan } = mock
reader.setAttribute('metadata-url', 'https://orson-welles.io')
let resolveLoadingEnded = undefined
const loadingEnded = new Promise((resolve) => resolveLoadingEnded = resolve)
fetch.mockImplementationOnce(async (url) => {
return mockFetch(url)
})
await scan(qrCodeData)
// Wait for scan promises to finish
await new Promise((resolve) => setTimeout(resolve))
fetch.mockReset()
}
await use(loadMetadata)
}
})
qrcodeReaderMetadataTest('qrcode reader fetches metadata', async ({mock, loadMetadata}) => {
const {reader} = mock
await loadMetadata(okCodeData, async (url) => {
expect(url).toBe('https://orson-welles.io?certificate=853b9497-6c1a-4ff1-8604-6dc7cca9003c')
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).not.toBe(null)
return {
ok: true,
json: () => new Promise((resolve) => {
resolve({'err': 0, 'data': {'Classe': 'Oui', 'Ravioles': 'Non'}})
})
}
})
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).toBe(null)
const labels = reader.querySelectorAll('.qrcode-reader--metadata-items .qrcode-reader--data-item-label')
expect(labels.length).toBe(2)
expect(labels[0].innerText).toMatch('Classe')
expect(labels[1].innerText).toMatch('Ravioles')
const values = reader.querySelectorAll('.qrcode-reader--metadata-items .qrcode-reader--data-item-value')
expect(values.length).toBe(2)
expect(values[0].innerText).toMatch('Oui')
expect(values[1].innerText).toMatch('Non')
})
qrcodeReaderMetadataTest('qrcode reader show feedback on metadata network error', async ({mock, loadMetadata}) => {
const { reader, scan } = mock
await loadMetadata(okCodeData, async (url) => {
throw { name: 'NetworkError' }
})
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).toBe(null)
const items = reader.querySelector('.qrcode-reader--metadata-items')
expect(items.classList.contains('error')).toBe(true)
expect(items.innerText).toBe('metadata_network_error')
})
qrcodeReaderMetadataTest('qrcode reader show feedback on http error', async ({mock, loadMetadata}) => {
const { reader, scan } = mock
await loadMetadata(okCodeData, async (url) => {
return {
ok: false,
json: () => new Promise((resolve) => resolve({}))
}
})
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).toBe(null)
const items = reader.querySelector('.qrcode-reader--metadata-items')
expect(items.classList.contains('error')).toBe(true)
expect(items.innerText.trim()).toBe('metadata_api_error')
})
qrcodeReaderMetadataTest('qrcode reader show feedback on api error', async ({mock, loadMetadata}) => {
const { reader, scan } = mock
await loadMetadata(okCodeData, async (url) => {
return {
ok: true,
json: () => new Promise((resolve) => {
resolve({'err': 1})
})
}
})
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).toBe(null)
const items = reader.querySelector('.qrcode-reader--metadata-items')
expect(items.classList.contains('error')).toBe(true)
expect(items.innerText.trim()).toBe('metadata_api_error')
})
qrcodeReaderMetadataTest("qrcode reader doesn't refetch metadata for the same certificate if popup is opened", async ({mock, loadMetadata}) => {
const { reader, scan } = mock
await loadMetadata(okCodeData, async (url) => {
return {
ok: true,
json: async () => ({'err': 0, 'data': {'Classe': 'Oui', 'Ravioles': 'Non'}})
}
})
let items = reader.querySelector('.qrcode-reader--metadata-items')
// loading the same certificate doesn't refeches metadata
await loadMetadata(okCodeData, async (url) => {
expect.unreachable()
})
expect(reader.querySelector('.qrcode-reader--metadata-items')).toBe(items)
// loading another certificate refetches metadata
await loadMetadata(certificateWithoutValidity, async (url) => {
return {
ok: true,
json: async () => ({'err': 0, 'data': {'Classe': 'Oui', 'Ravioles': 'Non'}})
}
})
expect(reader.querySelector('.qrcode-reader--metadata-items')).not.toBe(items)
items = reader.querySelector('.qrcode-reader--metadata-items')
const closeButton = reader.querySelector('.qrcode-reader--close-popup-button')
closeButton.dispatchEvent(new Event('click'))
// metadata is refetched for the same certificate after closing the popup
await loadMetadata(certificateWithoutValidity, async (url) => {
return {
ok: true,
json: async () => ({'err': 0, 'data': {'Classe': 'Oui', 'Ravioles': 'Non'}})
}
})
expect(reader.querySelector('.qrcode-reader--metadata-items')).not.toBe(items)
})
const qrcodeReaderTallyTest = qrcodeReaderTest.extend({
loadTally: async ({ task, mock }, use) => {
const loadTally = async (qrCodeData, mockFetch) => {
const { reader, scan } = mock
reader.setAttribute('tally-url', 'https://orson-welles.io')
let resolveLoadingEnded = undefined
fetch.mockImplementationOnce(async (url, {body}) => {
return mockFetch(url, JSON.parse(body))
})
await scan(qrCodeData)
await new Promise((resolve) => setTimeout(resolve))
fetch.mockReset()
}
await use(loadTally)
}
})
qrcodeReaderTallyTest('qrcode reader show tally status for unstamped certificates', async ({mock, loadTally}) => {
const { reader, scan } = mock
const nowBackup = Date.now
Date.now = () => 25000
await loadTally(okCodeData, async (url, body) => {
expect(body).toStrictEqual({
events: [{
certificate: '853b9497-6c1a-4ff1-8604-6dc7cca9003c',
timestamp: 25
}]
})
return {
ok: true,
json: async () => ({'err': 0, 'data': {'stamps': {[body.events[0].certificate]: 'ok'}}})
}
})
Date.now = nowBackup
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).toBe(null)
const items = reader.querySelector('.qrcode-reader--tally-status')
expect(items.classList.contains('error')).toBe(false)
expect(items.innerText.trim()).toBe('tally_status_ok')
})
qrcodeReaderTallyTest('qrcode reader show error for stamped certificates', async ({mock, loadTally}) => {
const { reader, scan } = mock
await loadTally(okCodeData, async (url, body) => {
return {
ok: true,
json: async () => ({'err': 0, 'data': {'stamps': {[body.events[0].certificate]: 'duplicate'}}})
}
})
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).toBe(null)
const items = reader.querySelector('.qrcode-reader--tally-status')
expect(items.classList.contains('error')).toBe(true)
expect(items.innerText.trim()).toBe('tally_status_already_seen')
})
qrcodeReaderTallyTest('qrcode reader show tally when request fails ', async ({mock, loadTally}) => {
const { reader, scan } = mock
await loadTally(okCodeData, async (url, body) => {
throw { name: 'NetworkError' }
})
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).toBe(null)
let items = reader.querySelector('.qrcode-reader--tally-status')
expect(items.classList.contains('error')).toBe(true)
expect(items.innerText.trim()).toBe('tally_network_error')
const closeButton = reader.querySelector('.qrcode-reader--close-popup-button')
closeButton.dispatchEvent(new Event('click'))
await loadTally(okCodeData, async (url, body) => {
return {
ok: false,
}
})
items = reader.querySelector('.qrcode-reader--tally-status')
expect(items.classList.contains('error')).toBe(true)
expect(items.innerText.trim()).toBe('tally_api_error')
})
qrcodeReaderTallyTest("qrcode reader doesn't refetch tally for the same certificate if popup is opened", async ({mock, loadTally}) => {
const { reader, scan } = mock
await loadTally(okCodeData, async (url, body) => {
return {
ok: true,
json: async () => ({'err': 0, 'data': {'stamps': {[body.events[0].certificate]: 'duplicate'}}})
}
})
let tallyStatus = reader.querySelector('.qrcode-reader--tally-status')
// loading the same certificate doesn't refeches metadata
await loadTally(okCodeData, async () => {
expect.unreachable()
})
expect(reader.querySelector('.qrcode-reader--tally-status')).toBe(tallyStatus)
// loading another certificate refetches tally status
await loadTally(certificateWithoutValidity, async (url, body) => {
return {
ok: true,
json: async () => ({'err': 0, 'data': {'stamps': {[body.events[0].certificate]: 'ok'}}})
}
})
expect(reader.querySelector('.qrcode-reader--tally-status')).not.toBe(tallyStatus)
tallyStatus = reader.querySelector('.qrcode-reader--tally-status')
const closeButton = reader.querySelector('.qrcode-reader--close-popup-button')
closeButton.dispatchEvent(new Event('click'))
// tally status is refetched for the same certificate after closing the popup
await loadTally(certificateWithoutValidity, async (url, body) => {
return {
ok: true,
json: async () => ({'err': 0, 'data': {'stamps': {[body.events[0].certificate]: 'ok'}}})
}
})
expect(reader.querySelector('.qrcode-reader--tally-status')).not.toBe(tallyStatus)
tallyStatus = reader.querySelector('.qrcode-reader--tally-status')
})