Skip to content

Commit a060afd

Browse files
Merge pull request #641 from adamtheturtle/query-extra-date-format
Query extra date format
2 parents 9c74516 + a3af46c commit a060afd

File tree

3 files changed

+137
-20
lines changed

3 files changed

+137
-20
lines changed

README.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,14 @@ This timeframe is not consistent on the real Vuforia Web Services.
185185
On the mock, this timeframe is three seconds by default.
186186
``MockVWS`` takes a parameter ``query_recognizes_deletion_seconds`` to change this.
187187

188+
Accepted date formats for the Query API
189+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
190+
191+
The Query API documentation is not clear on which date formats are expected exactly in the ``Date`` header.
192+
The mock is strict.
193+
That is, it accepts only a few date formats, and rejects all others.
194+
If you find a date format which is accepted by the real Query API but rejected by the mock, please create a GitHub issue.
195+
188196
.. |Build Status| image:: https://travis-ci.org/adamtheturtle/vws-python.svg?branch=master
189197
:target: https://travis-ci.org/adamtheturtle/vws-python
190198
.. |codecov| image:: https://codecov.io/gh/adamtheturtle/vws-python/branch/master/graph/badge.svg

src/mock_vws/_mock_web_query_api.py

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,27 @@ def validate_include_target_data(
277277
return unexpected_target_data_message
278278

279279

280+
def _accepted_date_formats() -> Set[str]:
281+
"""
282+
Return all known accepted date formats.
283+
284+
We expect that more formats than this will be accepted.
285+
These are the accepted ones we know of at the time of writing.
286+
"""
287+
known_accepted_formats = {
288+
'%a, %b %d %H:%M:%S %Y',
289+
'%a %b %d %H:%M:%S %Y',
290+
'%a, %d %b %Y %H:%M:%S',
291+
'%a %d %b %Y %H:%M:%S',
292+
}
293+
294+
known_accepted_formats = known_accepted_formats.union(
295+
set(date_format + ' GMT' for date_format in known_accepted_formats),
296+
)
297+
298+
return known_accepted_formats
299+
300+
280301
@wrapt.decorator
281302
def validate_date_format(
282303
wrapped: Callable[..., str],
@@ -298,21 +319,22 @@ def validate_date_format(
298319
An `UNAUTHORIZED` response if the date is in the wrong format.
299320
"""
300321
request, context = args
301-
302-
try:
303-
datetime.datetime.strptime(
304-
request.headers['Date'],
305-
'%a, %d %b %Y %H:%M:%S GMT',
306-
)
307-
except ValueError:
308-
context.status_code = codes.UNAUTHORIZED
309-
context.headers['WWW-Authenticate'] = 'VWS'
310-
text = 'Malformed date header.'
311-
content_type = 'text/plain; charset=ISO-8859-1'
312-
context.headers['Content-Type'] = content_type
313-
return text
314-
315-
return wrapped(*args, **kwargs)
322+
date_header = request.headers['Date']
323+
324+
for date_format in _accepted_date_formats():
325+
try:
326+
datetime.datetime.strptime(date_header, date_format)
327+
except ValueError:
328+
pass
329+
else:
330+
return wrapped(*args, **kwargs)
331+
332+
context.status_code = codes.UNAUTHORIZED
333+
context.headers['WWW-Authenticate'] = 'VWS'
334+
text = 'Malformed date header.'
335+
content_type = 'text/plain; charset=ISO-8859-1'
336+
context.headers['Content-Type'] = content_type
337+
return text
316338

317339

318340
@wrapt.decorator
@@ -336,15 +358,19 @@ def validate_date(
336358
A `FORBIDDEN` response if the date is out of range.
337359
"""
338360
request, context = args
361+
date_header = request.headers['Date']
339362

340-
date_from_header = datetime.datetime.strptime(
341-
request.headers['Date'],
342-
'%a, %d %b %Y %H:%M:%S GMT',
343-
)
363+
for date_format in _accepted_date_formats():
364+
try:
365+
date = datetime.datetime.strptime(date_header, date_format)
366+
except ValueError:
367+
pass
368+
else:
369+
break
344370

345371
gmt = pytz.timezone('GMT')
346372
now = datetime.datetime.now(tz=gmt)
347-
date_from_header = date_from_header.replace(tzinfo=gmt)
373+
date_from_header = date.replace(tzinfo=gmt)
348374
time_difference = now - date_from_header
349375

350376
maximum_time_difference = datetime.timedelta(minutes=65)

tests/mock_vws/test_query.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
import base64
88
import calendar
9+
import datetime
910
import io
1011
import time
1112
from typing import Dict, Union
1213
from urllib.parse import urljoin
1314

1415
import pytest
16+
import pytz
1517
import requests
1618
from PIL import Image
1719
from requests import codes
@@ -1600,3 +1602,84 @@ def test_status_failed(
16001602

16011603
assert_query_success(response=response)
16021604
assert response.json()['results'] == []
1605+
1606+
1607+
@pytest.mark.usefixtures('verify_mock_vuforia')
1608+
class TestDateFormats:
1609+
"""
1610+
Tests for various date formats.
1611+
1612+
The date format for the VWS API as per
1613+
https://library.vuforia.com/articles/Training/Using-the-VWS-API.html must
1614+
be in the rfc1123-date format.
1615+
1616+
However, for the query endpoint, the documentation does not mention the
1617+
format. It says:
1618+
1619+
> The data format must exactly match the Date that is sent in the ‘Date’
1620+
> header.
1621+
"""
1622+
1623+
@pytest.mark.parametrize(
1624+
'datetime_format',
1625+
[
1626+
'%a, %b %d %H:%M:%S %Y',
1627+
'%a %b %d %H:%M:%S %Y',
1628+
'%a, %d %b %Y %H:%M:%S',
1629+
'%a %d %b %Y %H:%M:%S',
1630+
],
1631+
)
1632+
@pytest.mark.parametrize('include_tz', [True, False])
1633+
def test_date_formats(
1634+
self,
1635+
high_quality_image: io.BytesIO,
1636+
vuforia_database_keys: VuforiaDatabaseKeys,
1637+
datetime_format: str,
1638+
include_tz: bool,
1639+
) -> None:
1640+
"""
1641+
Test various date formats which are known to be accepted.
1642+
1643+
We expect that more formats than this will be accepted.
1644+
These are the accepted ones we know of at the time of writing.
1645+
"""
1646+
image_content = high_quality_image.getvalue()
1647+
body = {'image': ('image.jpeg', image_content, 'image/jpeg')}
1648+
1649+
if include_tz:
1650+
datetime_format += ' GMT'
1651+
1652+
gmt = pytz.timezone('GMT')
1653+
now = datetime.datetime.now(tz=gmt)
1654+
date = now.strftime(datetime_format)
1655+
request_path = '/v1/query'
1656+
content, content_type_header = encode_multipart_formdata(body)
1657+
method = POST
1658+
1659+
access_key = vuforia_database_keys.client_access_key
1660+
secret_key = vuforia_database_keys.client_secret_key
1661+
authorization_string = authorization_header(
1662+
access_key=access_key,
1663+
secret_key=secret_key,
1664+
method=method,
1665+
content=content,
1666+
content_type='multipart/form-data',
1667+
date=date,
1668+
request_path=request_path,
1669+
)
1670+
1671+
headers = {
1672+
'Authorization': authorization_string,
1673+
'Date': date,
1674+
'Content-Type': content_type_header,
1675+
}
1676+
1677+
response = requests.request(
1678+
method=method,
1679+
url=urljoin(base=VWQ_HOST, url=request_path),
1680+
headers=headers,
1681+
data=content,
1682+
)
1683+
1684+
assert_query_success(response=response)
1685+
assert response.json()['results'] == []

0 commit comments

Comments
 (0)