Skip to content

Commit 00ccec3

Browse files
committed
Merge remote-tracking branch 'origin/master' into metadata-too-large
2 parents 3eac4ac + a060afd commit 00ccec3

File tree

3 files changed

+163
-16
lines changed

3 files changed

+163
-16
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: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -277,15 +277,36 @@ 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
281-
def validate_date(
302+
def validate_date_format(
282303
wrapped: Callable[..., str],
283304
instance: Any, # pylint: disable=unused-argument
284305
args: Tuple[_RequestObjectProxy, _Context],
285306
kwargs: Dict,
286307
) -> str:
287308
"""
288-
Validate the date header given to the query endpoint.
309+
Validate the format of the date header given to the query endpoint.
289310
290311
Args:
291312
wrapped: An endpoint function for `requests_mock`.
@@ -296,26 +317,60 @@ def validate_date(
296317
Returns:
297318
The result of calling the endpoint.
298319
An `UNAUTHORIZED` response if the date is in the wrong format.
320+
"""
321+
request, context = args
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
338+
339+
340+
@wrapt.decorator
341+
def validate_date(
342+
wrapped: Callable[..., str],
343+
instance: Any, # pylint: disable=unused-argument
344+
args: Tuple[_RequestObjectProxy, _Context],
345+
kwargs: Dict,
346+
) -> str:
347+
"""
348+
Validate date in the date header given to the query endpoint.
349+
350+
Args:
351+
wrapped: An endpoint function for `requests_mock`.
352+
instance: The class that the endpoint function is in.
353+
args: The arguments given to the endpoint function.
354+
kwargs: The keyword arguments given to the endpoint function.
355+
356+
Returns:
357+
The result of calling the endpoint.
299358
A `FORBIDDEN` response if the date is out of range.
300359
"""
301360
request, context = args
361+
date_header = request.headers['Date']
302362

303-
try:
304-
date_from_header = datetime.datetime.strptime(
305-
request.headers['Date'],
306-
'%a, %d %b %Y %H:%M:%S GMT',
307-
)
308-
except ValueError:
309-
context.status_code = codes.UNAUTHORIZED
310-
context.headers['WWW-Authenticate'] = 'VWS'
311-
text = 'Malformed date header.'
312-
content_type = 'text/plain; charset=ISO-8859-1'
313-
context.headers['Content-Type'] = content_type
314-
return text
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
315370

316371
gmt = pytz.timezone('GMT')
317372
now = datetime.datetime.now(tz=gmt)
318-
date_from_header = date_from_header.replace(tzinfo=gmt)
373+
date_from_header = date.replace(tzinfo=gmt)
319374
time_difference = now - date_from_header
320375

321376
maximum_time_difference = datetime.timedelta(minutes=65)
@@ -527,6 +582,7 @@ def decorator(method: Callable[..., str]) -> Callable[..., str]:
527582
decorators = [
528583
validate_authorization,
529584
validate_date,
585+
validate_date_format,
530586
validate_date_header_given,
531587
validate_include_target_data,
532588
validate_max_num_results,

tests/mock_vws/test_query.py

Lines changed: 84 additions & 1 deletion
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
@@ -525,7 +527,7 @@ def test_default(
525527
assert len(response.json()['results']) == 1
526528

527529
@pytest.mark.parametrize('num_results', [1, b'1', 50])
528-
def test_valid_foo(
530+
def test_valid_accepted(
529531
self,
530532
high_quality_image: io.BytesIO,
531533
vuforia_database_keys: VuforiaDatabaseKeys,
@@ -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)