This commit is contained in:
parent
cd0f441d3b
commit
36dfa9508e
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 3.2.18 on 2023-11-14 16:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('qrcode', '0004_validity_is_optional'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='certificate',
|
||||
name='metadata',
|
||||
field=models.JSONField(null=True, verbose_name='Certificate meta data'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='reader',
|
||||
name='readable_metadatas',
|
||||
field=models.CharField(max_length=128, null=True, verbose_name='Readable metadata keys'),
|
||||
),
|
||||
]
|
|
@ -30,6 +30,11 @@ CERTIFICATE_SCHEMA = {
|
|||
'title': _('Data to encode in the certificate'),
|
||||
'additionalProperties': {'type': 'string'},
|
||||
},
|
||||
'metadata': {
|
||||
'type': 'object',
|
||||
'title': _('Metadata associated to the certificate'),
|
||||
'additionalProperties': {'type': 'string'},
|
||||
},
|
||||
'validity_start': {
|
||||
'any': [{'type': 'null'}, {'const': ''}, {'type': 'string', 'format': 'date-time'}],
|
||||
},
|
||||
|
@ -44,6 +49,10 @@ READER_SCHEMA = {
|
|||
'type': 'object',
|
||||
'additionalProperties': False,
|
||||
'properties': {
|
||||
'readable_metadatas': {
|
||||
'title': _('Comma-separated list of metadata keys that this reader is allowed to read.'),
|
||||
'any': [{'type': 'null'}, {'const': ''}, {'type': 'string'}],
|
||||
},
|
||||
'validity_start': {
|
||||
'any': [{'type': 'null'}, {'const': ''}, {'type': 'string', 'format': 'date-time'}],
|
||||
},
|
||||
|
@ -108,10 +117,12 @@ class QRCodeConnector(BaseResource):
|
|||
else:
|
||||
validity_end = None
|
||||
data = post_data.get('data') or {}
|
||||
metadata = post_data.get('metadata') or {}
|
||||
|
||||
if not uuid:
|
||||
certificate = self.certificates.create(
|
||||
data=data,
|
||||
metadata=metadata,
|
||||
validity_start=validity_start,
|
||||
validity_end=validity_end,
|
||||
)
|
||||
|
@ -120,6 +131,7 @@ class QRCodeConnector(BaseResource):
|
|||
certificate.validity_start = validity_start
|
||||
certificate.validity_end = validity_end
|
||||
certificate.data = data
|
||||
certificate.metadata = metadata
|
||||
certificate.save()
|
||||
|
||||
return {
|
||||
|
@ -194,15 +206,19 @@ class QRCodeConnector(BaseResource):
|
|||
else:
|
||||
validity_end = None
|
||||
|
||||
readable_metadatas = post_data.get('readable_metadatas', '')
|
||||
|
||||
if not uuid:
|
||||
reader = self.readers.create(
|
||||
validity_start=validity_start,
|
||||
validity_end=validity_end,
|
||||
readable_metadatas=readable_metadatas,
|
||||
)
|
||||
else:
|
||||
reader = get_object_or_404(self.readers, uuid=uuid)
|
||||
reader.validity_start = validity_start
|
||||
reader.validity_end = validity_end
|
||||
reader.readable_metadatas = readable_metadatas
|
||||
reader.save()
|
||||
|
||||
return {
|
||||
|
@ -252,6 +268,7 @@ class QRCodeConnector(BaseResource):
|
|||
def open_reader(self, request, uuid):
|
||||
reader = get_object_or_404(self.readers, uuid=uuid)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
return TemplateResponse(
|
||||
request,
|
||||
'qrcode/qrcode-reader.html',
|
||||
|
@ -260,9 +277,40 @@ class QRCodeConnector(BaseResource):
|
|||
'expired': now >= reader.validity_end if reader.validity_end is not None else False,
|
||||
'verify_key': self.hex_verify_key,
|
||||
'reader': reader,
|
||||
'metadata_url': reader.get_metadata_url(request),
|
||||
},
|
||||
)
|
||||
|
||||
@endpoint(
|
||||
name='read-metadata',
|
||||
perm='OPEN',
|
||||
description=_('Read certificate metadata'),
|
||||
pattern=f'^{UUID_PATTERN}$',
|
||||
example_pattern='{uuid}',
|
||||
parameters={
|
||||
'uuid': {
|
||||
'description': _('QRCode reader identifier'),
|
||||
'example_value': '12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
'certificate': {
|
||||
'description': _('Certificate identifier'),
|
||||
'example_value': '12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
},
|
||||
)
|
||||
def read_metadata(self, request, uuid, certificate):
|
||||
reader = get_object_or_404(self.readers, uuid=uuid)
|
||||
certificate = get_object_or_404(self.certificates, uuid=certificate)
|
||||
now = datetime.now(timezone.utc)
|
||||
if reader.validity_start is not None and now < reader.validity_start:
|
||||
return {'err': 1, 'err_desc': _("Reader isn't usable yet.")}
|
||||
|
||||
if reader.validity_end is not None and now > reader.validity_end:
|
||||
return {'err': 1, 'err_desc': _('Reader has expired.')}
|
||||
|
||||
readable_metatadas = reader.readable_metadatas.split(',')
|
||||
return {'err': 0, 'data': {k: v for k, v in certificate.metadata.items() if k in readable_metatadas}}
|
||||
|
||||
|
||||
def encode_mime_like(data):
|
||||
msg = ''
|
||||
|
@ -297,6 +345,7 @@ class Certificate(models.Model):
|
|||
validity_start = models.DateTimeField(verbose_name=_('Validity Start Date'), null=True)
|
||||
validity_end = models.DateTimeField(verbose_name=_('Validity End Date'), null=True)
|
||||
data = models.JSONField(null=True, verbose_name='Certificate Data')
|
||||
metadata = models.JSONField(null=True, verbose_name='Certificate meta data')
|
||||
resource = models.ForeignKey(QRCodeConnector, on_delete=models.CASCADE, related_name='certificates')
|
||||
|
||||
def to_json(self):
|
||||
|
@ -343,15 +392,24 @@ class Reader(models.Model):
|
|||
modified = models.DateTimeField(verbose_name=_('Last modification'), auto_now=True)
|
||||
validity_start = models.DateTimeField(verbose_name=_('Validity Start Date'), null=True)
|
||||
validity_end = models.DateTimeField(verbose_name=_('Validity End Date'), null=True)
|
||||
readable_metadatas = models.CharField(max_length=128, verbose_name=_('Readable metadata keys'), null=True)
|
||||
resource = models.ForeignKey(QRCodeConnector, on_delete=models.CASCADE, related_name='readers')
|
||||
|
||||
def get_url(self, request):
|
||||
return self._get_endpoint_url(request, 'open-reader')
|
||||
|
||||
def get_metadata_url(self, request):
|
||||
if not self.readable_metadatas:
|
||||
return None
|
||||
return self._get_endpoint_url(request, 'read-metadata')
|
||||
|
||||
def _get_endpoint_url(self, request, endpoint):
|
||||
relative_url = reverse(
|
||||
'generic-endpoint',
|
||||
kwargs={
|
||||
'slug': self.resource.slug,
|
||||
'connector': self.resource.get_connector_slug(),
|
||||
'endpoint': 'open-reader',
|
||||
'endpoint': endpoint,
|
||||
'rest': str(self.uuid),
|
||||
},
|
||||
)
|
||||
|
|
|
@ -119,12 +119,16 @@ qrcode-reader {
|
|||
align-self: end;
|
||||
}
|
||||
|
||||
&--data-items {
|
||||
&--data-items, &--metadata-items {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
margin-top: 5px;
|
||||
padding-top: 5px;
|
||||
border-top: 1px solid var(--gray-light);
|
||||
&.error {
|
||||
color: #{$red};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--data-item-label {
|
||||
|
@ -142,4 +146,18 @@ qrcode-reader {
|
|||
padding: 5px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
&--spinner {
|
||||
grid-area: 1 / 1 / 2 / 3;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
&--spinner-animation {
|
||||
transform-origin:center;
|
||||
animation:spinner-keyframes .75s infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner-keyframes {
|
||||
100% { transform:rotate(360deg); }
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ const validityTemplate = template(`
|
|||
|
||||
const dataTemplate = template(`
|
||||
<div class="qrcode-reader--data-items"></div>
|
||||
<span class="qrcode-reader--metadata-items"></span>
|
||||
`)
|
||||
|
||||
const dataItemTemplate = template(`
|
||||
|
@ -69,6 +70,15 @@ const dataItemTemplate = template(`
|
|||
<span class="qrcode-reader--data-item-value">{value}</span>
|
||||
`)
|
||||
|
||||
const spinnerTemplate = template(`
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="qrcode-reader--spinner">
|
||||
<path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25"/>
|
||||
<path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="qrcode-reader--spinner-animation"/>
|
||||
</svg>
|
||||
`)
|
||||
|
||||
function decodeMimeLike (value) {
|
||||
const chunks = value.split('\n')
|
||||
const data = {}
|
||||
|
@ -221,6 +231,7 @@ class QRCodeReader extends window.HTMLElement {
|
|||
const decoder = new TextDecoder('utf-8')
|
||||
const decoded = decoder.decode(opened)
|
||||
const data = decodeMimeLike(decoded)
|
||||
const certificateUUID = data.uuid
|
||||
|
||||
delete data.uuid
|
||||
|
||||
|
@ -260,6 +271,7 @@ class QRCodeReader extends window.HTMLElement {
|
|||
const dataElement = dataTemplate.cloneNode(true)
|
||||
|
||||
const dataItems = dataElement.content.querySelector('.qrcode-reader--data-items')
|
||||
const metadataItems = dataElement.content.querySelector('.qrcode-reader--metadata-items')
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const dataItem = dataItemTemplate.cloneNode(true)
|
||||
|
@ -268,6 +280,57 @@ class QRCodeReader extends window.HTMLElement {
|
|||
}
|
||||
|
||||
this.#popupContent.append(dataElement.content)
|
||||
|
||||
if(this.getAttribute('metadata-url')) {
|
||||
this.#showMetadata(metadataItems, certificateUUID)
|
||||
}
|
||||
}
|
||||
|
||||
async #showMetadata(itemsElement, certificateUUID) {
|
||||
const spinner = spinnerTemplate.cloneNode(true)
|
||||
itemsElement.appendChild(spinner.content)
|
||||
|
||||
const metadata = await this.#fetchMetadata(itemsElement, certificateUUID)
|
||||
if(!metadata) {
|
||||
return
|
||||
}
|
||||
|
||||
itemsElement.innerText = ""
|
||||
|
||||
for (const [key, value] of Object.entries(metadata)) {
|
||||
const dataItem = dataItemTemplate.cloneNode(true)
|
||||
dataItem.innerHTML = dataItem.innerHTML.replace('{label}', key).replace('{value}', value)
|
||||
itemsElement.append(dataItem.content)
|
||||
}
|
||||
}
|
||||
|
||||
async #fetchMetadata(itemsElement, certificateUUID) {
|
||||
const url = `${this.getAttribute('metadata-url')}?certificate=${certificateUUID}`
|
||||
let metadata = undefined
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const content = await response.json()
|
||||
|
||||
if(response.ok && content.err == 0) {
|
||||
metadata = content.data
|
||||
}
|
||||
else {
|
||||
itemsElement.classList.add('error')
|
||||
itemsElement.innerHTML = `
|
||||
<p>${translate('metadata_api_error')}</p>
|
||||
`
|
||||
}
|
||||
} catch(err) {
|
||||
if(err.name == 'NetworkError') {
|
||||
itemsElement.classList.add('error')
|
||||
itemsElement.innerHTML = `<p>${translate('metadata_network_error')}</p>`
|
||||
}
|
||||
else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
#showError (message) {
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
"always": "Toujours",
|
||||
"close": "{% trans 'Close' %}",
|
||||
"expired": "{% trans 'QR code Expired' %}",
|
||||
"metadata_network_error": "{% trans 'A network error occured while fetching metadata : check network connectivity.' %}",
|
||||
"metadata_api_error": "{% trans 'An api error occured while fetching metadata.' %}",
|
||||
"from": "{% trans 'From' %}",
|
||||
"invalid_content": "{% trans "This QR code isn't supported by this application." %}",
|
||||
"invalid_signature": "{% trans 'Signature verification failed.' %}",
|
||||
|
@ -32,7 +34,9 @@
|
|||
{% elif expired %}
|
||||
{% trans "Reader has expired." %}
|
||||
{% else %}
|
||||
<qrcode-reader verify-key="{{ verify_key }}"></qrcode-reader>
|
||||
<qrcode-reader verify-key="{{ verify_key }}"
|
||||
{% if metadata_url %}metadata-url="{{metadata_url}}"{% endif %}
|
||||
></qrcode-reader>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -63,6 +63,9 @@ const qrcodeReaderTest = test.extend({
|
|||
reader.setAttribute('verify-key', '15dbdd38a2d8a2db1b4dd985da8da2b4e6b785b28db3fe0b34a10cfb3ba0aeb3')
|
||||
document.append(reader)
|
||||
|
||||
const fetchBackup = global.fetch
|
||||
global.fetch = vi.fn()
|
||||
|
||||
await use({
|
||||
reader,
|
||||
scan: async (text) => {
|
||||
|
@ -73,6 +76,8 @@ const qrcodeReaderTest = test.extend({
|
|||
})
|
||||
vi.useFakeTimers()
|
||||
|
||||
global.fetch = fetchBackup
|
||||
|
||||
reader.remove()
|
||||
|
||||
vi.useRealTimers ()
|
||||
|
@ -235,3 +240,109 @@ qrcodeReaderTest('qrcode reader accepts certificate without validity dates', asy
|
|||
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) => {
|
||||
const observer = new MutationObserver((_, observer) => {
|
||||
resolveLoadingEnded()
|
||||
observer.disconnect()
|
||||
})
|
||||
observer.observe(reader.querySelector(".qrcode-reader--metadata-items"), { childList: true })
|
||||
return mockFetch(url)
|
||||
})
|
||||
|
||||
await scan(qrCodeData)
|
||||
await loadingEnded
|
||||
}
|
||||
|
||||
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')
|
||||
})
|
||||
|
|
|
@ -43,6 +43,7 @@ def test_save_certificate(app, connector):
|
|||
'first_name': 'Georges',
|
||||
'last_name': 'Abitbol',
|
||||
},
|
||||
'metadata': {'puissance_intellectuelle': 'BAC +2'},
|
||||
'validity_start': '2022-01-01 10:00:00+00:00',
|
||||
'validity_end': '2023-01-01 10:00:00+00:00',
|
||||
},
|
||||
|
@ -56,6 +57,7 @@ def test_save_certificate(app, connector):
|
|||
|
||||
assert certificate.data['first_name'] == 'Georges'
|
||||
assert certificate.data['last_name'] == 'Abitbol'
|
||||
assert certificate.metadata['puissance_intellectuelle'] == 'BAC +2'
|
||||
assert certificate.validity_start == datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert certificate.validity_end == datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
@ -130,6 +132,7 @@ def test_save_reader(app, connector):
|
|||
params={
|
||||
'validity_start': '2022-01-01 10:00:00+00:00',
|
||||
'validity_end': '2023-01-01 10:00:00+00:00',
|
||||
'readable_metadatas': 'name,last_name',
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -141,6 +144,7 @@ def test_save_reader(app, connector):
|
|||
|
||||
assert reader.validity_start == datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert reader.validity_end == datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert reader.readable_metadatas == 'name,last_name'
|
||||
|
||||
result = app.post_json(
|
||||
f'{endpoint}/{reader_uuid}',
|
||||
|
@ -198,6 +202,40 @@ def test_open_reader(app, connector, freezer):
|
|||
assert 'Reader has expired.' in result.body.decode('utf-8')
|
||||
|
||||
|
||||
def test_read_metadata(app, connector, freezer):
|
||||
reader = connector.readers.create(
|
||||
readable_metadatas='first_name,puissance_intellectuelle',
|
||||
validity_start=datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
certificate = connector.certificates.create(
|
||||
uuid=uuid.UUID('12345678-1234-5678-1234-567812345678'),
|
||||
metadata={'first_name': 'Georges', 'last_name': 'Abitbol', 'puissance_intellectuelle': 'BAC +2'},
|
||||
validity_start=datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
endpoint = generic_endpoint_url('qrcode', 'read-metadata', slug=connector.slug)
|
||||
freezer.move_to('2022-01-01T09:59:59')
|
||||
result = app.get(f'{endpoint}/{reader.uuid}', params={'certificate': certificate.uuid})
|
||||
|
||||
assert result.json['err'] == 1
|
||||
assert result.json['err_desc'] == 'Reader isn\'t usable yet.'
|
||||
|
||||
freezer.move_to('2022-01-01T10:00:00')
|
||||
result = app.get(f'{endpoint}/{reader.uuid}', params={'certificate': certificate.uuid})
|
||||
|
||||
assert result.json['err'] == 0
|
||||
assert result.json['data'] == {'first_name': 'Georges', 'puissance_intellectuelle': 'BAC +2'}
|
||||
|
||||
freezer.move_to('2023-01-01T10:00:01')
|
||||
result = app.get(f'{endpoint}/{reader.uuid}', params={'certificate': certificate.uuid})
|
||||
|
||||
assert result.json['err'] == 1
|
||||
assert result.json['err_desc'] == 'Reader has expired.'
|
||||
|
||||
|
||||
MISSING = object()
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue