def test_date_in_range( self, time_multiplier: int, endpoint: Endpoint, ) -> None: """ If a date header is within five minutes before or after the request is sent, no error is returned. Because there is a small delay in sending requests and Vuforia isn't consistent, some leeway is given. """ url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc skew = { 'vws.vuforia.com': _VWS_MAX_TIME_SKEW, 'cloudreco.vuforia.com': _VWQ_MAX_TIME_SKEW, }[netloc] time_difference_from_now = skew - _LEEWAY time_difference_from_now *= time_multiplier gmt = ZoneInfo('GMT') with freeze_time(datetime.now(tz=gmt) + time_difference_from_now): date = rfc_1123_date() endpoint_headers = dict(endpoint.prepared_request.headers) content = endpoint.prepared_request.body or b'' assert isinstance(content, bytes) authorization_string = authorization_header( access_key=endpoint.access_key, secret_key=endpoint.secret_key, method=str(endpoint.prepared_request.method), content=content, content_type=endpoint.auth_header_content_type, date=date, request_path=endpoint.prepared_request.path_url, ) headers = { **endpoint_headers, 'Authorization': authorization_string, 'Date': date, } endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) session = requests.Session() response = session.send( # type: ignore request=endpoint.prepared_request, ) url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc if netloc == 'cloudreco.vuforia.com': assert_query_success(response=response) return assert_vws_response( response=response, status_code=endpoint.successful_headers_status_code, result_code=endpoint.successful_headers_result_code, )
def test_rfc_1123_date() -> None: """ The date is returned in the format described at https://library.vuforia.com/articles/Training/Using-the-VWS-API: ``` Date: This is the current date per RFC 2616, section 3.3.1, rfc1123-date format, for example, Sun, 22 Apr 2012 08:49:37 GMT, NOTE: The date and time always refer to GMT. ``` """ not_gmt_timezone = ZoneInfo('America/New_York') frozen_time = datetime.datetime( year=2015, month=2, day=5, hour=9, minute=55, second=12, microsecond=11, tzinfo=not_gmt_timezone, ) with freeze_time(frozen_time): result = vws_auth_tools.rfc_1123_date() assert result == 'Thu, 05 Feb 2015 14:55:12 GMT'
def test_missing_signature( self, endpoint: Endpoint, authorization_string: str, ) -> None: """ If a signature is missing `Authorization` header is given, a ``BAD_REQUEST`` response is given. """ date = rfc_1123_date() headers: Dict[str, str] = { **endpoint.prepared_request.headers, 'Authorization': authorization_string, 'Date': date, } endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) session = requests.Session() response = session.send(request=endpoint.prepared_request) url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc if netloc == 'cloudreco.vuforia.com': assert_vwq_failure( response=response, status_code=HTTPStatus.INTERNAL_SERVER_ERROR, content_type='text/html;charset=iso-8859-1', cache_control='must-revalidate,no-cache,no-store', www_authenticate=None, connection='keep-alive', ) content_filename = 'jetty_error_array_out_of_bounds.html' content_filename_2 = 'jetty_error_array_out_of_bounds_2.html' content_filename_3 = 'jetty_error_array_out_of_bounds_3.html' content_path = Path(__file__).parent / content_filename content_path_2 = Path(__file__).parent / content_filename_2 content_path_3 = Path(__file__).parent / content_filename_3 content_text = content_path.read_text() content_2_text = content_path_2.read_text() content_3_text = content_path_3.read_text() # We make a new variable for response text so that it is printed # with ``pytest --showlocals``. response_text = response.text assert response_text in ( content_text, content_2_text, content_3_text, ) return assert_vws_failure( response=response, status_code=HTTPStatus.BAD_REQUEST, result_code=ResultCodes.FAIL, )
def test_date_out_of_range( self, time_multiplier: int, endpoint: Endpoint, ) -> None: """ If the date header is more than five minutes (target API) or 65 minutes (query API) before or after the request is sent, a `FORBIDDEN` response is returned. Because there is a small delay in sending requests and Vuforia isn't consistent, some leeway is given. """ url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc skew = { 'vws.vuforia.com': _VWS_MAX_TIME_SKEW, 'cloudreco.vuforia.com': _VWQ_MAX_TIME_SKEW, }[netloc] time_difference_from_now = skew + _LEEWAY time_difference_from_now *= time_multiplier gmt = ZoneInfo('GMT') with freeze_time(datetime.now(tz=gmt) + time_difference_from_now): date = rfc_1123_date() endpoint_headers = dict(endpoint.prepared_request.headers) content = endpoint.prepared_request.body or b'' assert isinstance(content, bytes) authorization_string = authorization_header( access_key=endpoint.access_key, secret_key=endpoint.secret_key, method=str(endpoint.prepared_request.method), content=content, content_type=endpoint.auth_header_content_type, date=date, request_path=endpoint.prepared_request.path_url, ) headers = { **endpoint_headers, 'Authorization': authorization_string, 'Date': date, } endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) session = requests.Session() response = session.send( # type: ignore request=endpoint.prepared_request, ) # Even with the query endpoint, we get a JSON response. assert_vws_failure( response=response, status_code=HTTPStatus.FORBIDDEN, result_code=ResultCodes.REQUEST_TIME_TOO_SKEWED, )
def _add_target( vuforia_database: VuforiaDatabase, image_file_failed_state: io.BytesIO, ) -> Endpoint: """ Return details of the endpoint for adding a target. """ image_data = image_file_failed_state.read() image_data_encoded = base64.b64encode(image_data).decode('ascii') date = rfc_1123_date() data: Dict[str, Any] = { 'name': 'example_name', 'width': 1, 'image': image_data_encoded, } request_path = '/targets' content_type = 'application/json' method = POST content = bytes(json.dumps(data), encoding='utf-8') access_key = vuforia_database.server_access_key secret_key = vuforia_database.server_secret_key authorization_string = authorization_header( access_key=access_key, secret_key=secret_key, method=method, content=content, content_type=content_type, date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, 'Content-Type': content_type, } request = requests.Request( method=method, url=urljoin(base=VWS_HOST, url=request_path), headers=headers, data=content, ) prepared_request = request.prepare() return Endpoint( successful_headers_status_code=HTTPStatus.CREATED, successful_headers_result_code=ResultCodes.TARGET_CREATED, prepared_request=prepared_request, access_key=access_key, secret_key=secret_key, )
def _target_api_request( server_access_key: str, server_secret_key: str, method: str, content: bytes, request_path: str, base_vws_url: str, ) -> Response: """ Make a request to the Vuforia Target API. This uses `requests` to make a request against https://vws.vuforia.com. The content type of the request will be `application/json`. Args: server_access_key: A VWS server access key. server_secret_key: A VWS server secret key. method: The HTTP method which will be used in the request. content: The request body which will be used in the request. request_path: The path to the endpoint which will be used in the request. base_vws_url: The base URL for the VWS API. Returns: The response to the request made by `requests`. """ date_string = rfc_1123_date() content_type = 'application/json' signature_string = authorization_header( access_key=server_access_key, secret_key=server_secret_key, method=method, content=content, content_type=content_type, date=date_string, request_path=request_path, ) headers = { 'Authorization': signature_string, 'Date': date_string, 'Content-Type': content_type, } url = urljoin(base=base_vws_url, url=request_path) response = requests.request( method=method, url=url, headers=headers, data=content, ) return response
def request_mocked_address() -> None: """ Make a request, using `requests` to an address that is mocked by `MockVWS`. """ requests.get( url='https://vws.vuforia.com/summary', headers={ 'Date': rfc_1123_date(), 'Authorization': 'bad_auth_token', }, data=b'', )
def _query( vuforia_database: VuforiaDatabase, high_quality_image: io.BytesIO, ) -> Endpoint: """ Return details of the endpoint for making an image recognition query. """ image_content = high_quality_image.read() date = rfc_1123_date() request_path = '/v1/query' files = {'image': ('image.jpeg', image_content, 'image/jpeg')} method = POST content, content_type_header = encode_multipart_formdata( fields=files, ) # type: ignore access_key = vuforia_database.client_access_key secret_key = vuforia_database.client_secret_key authorization_string = authorization_header( access_key=access_key, secret_key=secret_key, method=method, content=content, # Note that this is not the actual Content-Type header value sent. content_type='multipart/form-data', date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, 'Content-Type': content_type_header, } request = requests.Request( method=method, url=urljoin(base=VWQ_HOST, url=request_path), headers=headers, data=content, ) prepared_request = request.prepare() return Endpoint( successful_headers_status_code=HTTPStatus.OK, successful_headers_result_code=ResultCodes.SUCCESS, prepared_request=prepared_request, access_key=access_key, secret_key=secret_key, )
def _update_target( vuforia_database: VuforiaDatabase, target_id: str, vws_client: VWS, ) -> Endpoint: """ Return details of the endpoint for updating a target. """ vws_client.wait_for_target_processed(target_id=target_id) data: Dict[str, Any] = {} request_path = f'/targets/{target_id}' content = bytes(json.dumps(data), encoding='utf-8') content_type = 'application/json' date = rfc_1123_date() method = PUT access_key = vuforia_database.server_access_key secret_key = vuforia_database.server_secret_key authorization_string = authorization_header( access_key=access_key, secret_key=secret_key, method=method, content=content, content_type=content_type, date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, 'Content-Type': content_type, } request = requests.Request( method=method, url=urljoin(base=VWS_HOST, url=request_path), headers=headers, data=content, ) prepared_request = request.prepare() return Endpoint( successful_headers_status_code=HTTPStatus.OK, successful_headers_result_code=ResultCodes.SUCCESS, prepared_request=prepared_request, access_key=access_key, secret_key=secret_key, )
def _get_duplicates( vuforia_database: VuforiaDatabase, target_id: str, vws_client: VWS, ) -> Endpoint: """ Return details of the endpoint for getting potential duplicates of a target. """ vws_client.wait_for_target_processed(target_id=target_id) date = rfc_1123_date() request_path = f'/duplicates/{target_id}' method = GET content = b'' access_key = vuforia_database.server_access_key secret_key = vuforia_database.server_secret_key authorization_string = authorization_header( access_key=access_key, secret_key=secret_key, method=method, content=content, content_type='', date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, } request = requests.Request( method=method, url=urljoin(base=VWS_HOST, url=request_path), headers=headers, data=content, ) prepared_request = request.prepare() return Endpoint( successful_headers_status_code=HTTPStatus.OK, successful_headers_result_code=ResultCodes.SUCCESS, prepared_request=prepared_request, access_key=access_key, secret_key=secret_key, )
def update_target( vuforia_database: VuforiaDatabase, data: Dict[str, Any], target_id: str, content_type: str = 'application/json', ) -> Response: """ Make a request to the endpoint to update a target. Args: vuforia_database: The credentials to use to connect to Vuforia. data: The data to send, in JSON format, to the endpoint. target_id: The ID of the target to update. content_type: The `Content-Type` header to use. Returns: The response returned by the API. """ date = rfc_1123_date() request_path = '/targets/' + target_id content = bytes(json.dumps(data), encoding='utf-8') authorization_string = authorization_header( access_key=vuforia_database.server_access_key, secret_key=vuforia_database.server_secret_key, method=PUT, content=content, content_type=content_type, date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, 'Content-Type': content_type, } response = requests.request( method=PUT, url=urljoin('https://vws.vuforia.com/', request_path), headers=headers, data=content, ) return response
def _target_list(vuforia_database: VuforiaDatabase) -> Endpoint: """ Return details of the endpoint for getting a list of targets. """ date = rfc_1123_date() request_path = '/targets' method = GET content = b'' access_key = vuforia_database.server_access_key secret_key = vuforia_database.server_secret_key authorization_string = authorization_header( access_key=access_key, secret_key=secret_key, method=method, content=content, content_type='', date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, } request = requests.Request( method=method, url=urljoin(base=VWS_HOST, url=request_path), headers=headers, data=content, ) prepared_request = request.prepare() return Endpoint( successful_headers_status_code=HTTPStatus.OK, successful_headers_result_code=ResultCodes.SUCCESS, prepared_request=prepared_request, access_key=access_key, secret_key=secret_key, )
def test_one_part_no_space( self, endpoint: Endpoint, authorization_string: str, ) -> None: """ A valid authorization string is two "parts" when split on a space. When a string is given which is one "part", a ``BAD_REQUEST`` or ``UNAUTHORIZED`` response is returned. """ date = rfc_1123_date() headers: Dict[str, str] = { **endpoint.prepared_request.headers, 'Authorization': authorization_string, 'Date': date, } endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) session = requests.Session() response = session.send(request=endpoint.prepared_request) url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc if netloc == 'cloudreco.vuforia.com': assert_vwq_failure( response=response, status_code=HTTPStatus.UNAUTHORIZED, content_type='text/plain;charset=iso-8859-1', cache_control=None, www_authenticate='VWS', connection='keep-alive', ) assert response.text == 'Malformed authorization header.' return assert_vws_failure( response=response, status_code=HTTPStatus.BAD_REQUEST, result_code=ResultCodes.FAIL, )
def test_missing_signature( self, endpoint: Endpoint, authorization_string: str, ) -> None: """ If a signature is missing `Authorization` header is given, a ``BAD_REQUEST`` response is given. """ date = rfc_1123_date() headers: Dict[str, str] = { **endpoint.prepared_request.headers, 'Authorization': authorization_string, 'Date': date, } endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) session = requests.Session() response = session.send( # type: ignore request=endpoint.prepared_request, ) url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc if netloc == 'cloudreco.vuforia.com': assert_vwq_failure( response=response, status_code=HTTPStatus.INTERNAL_SERVER_ERROR, content_type='text/html; charset=ISO-8859-1', ) # We have seen multiple responses given. assert 'Powered by Jetty' in response.text assert '500 Server Error' in response.text return assert_vws_failure( response=response, status_code=HTTPStatus.BAD_REQUEST, result_code=ResultCodes.FAIL, )
def test_missing(self, endpoint: Endpoint) -> None: """ An `UNAUTHORIZED` response is returned when no `Authorization` header is given. """ date = rfc_1123_date() endpoint_headers = dict(endpoint.prepared_request.headers) headers: Dict[str, str] = { **endpoint_headers, 'Date': date, } headers.pop('Authorization', None) endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) session = requests.Session() response = session.send(request=endpoint.prepared_request) url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc if netloc == 'cloudreco.vuforia.com': assert_vwq_failure( response=response, status_code=HTTPStatus.UNAUTHORIZED, content_type='text/plain;charset=iso-8859-1', cache_control=None, www_authenticate='VWS', connection='keep-alive', ) assert response.text == 'Authorization header missing.' return assert_vws_failure( response=response, status_code=HTTPStatus.UNAUTHORIZED, result_code=ResultCodes.AUTHENTICATION_FAILURE, )
def test_does_not_take_data( self, endpoint: Endpoint, ) -> None: """ Giving JSON to endpoints which do not take any JSON data returns error responses. """ if ( endpoint.prepared_request.headers.get( 'Content-Type', ) == 'application/json' ): return content = bytes(json.dumps({'key': 'value'}), encoding='utf-8') content_type = 'application/json' date = rfc_1123_date() endpoint_headers = dict(endpoint.prepared_request.headers) authorization_string = authorization_header( access_key=endpoint.access_key, secret_key=endpoint.secret_key, method=str(endpoint.prepared_request.method), content=content, content_type=content_type, date=date, request_path=endpoint.prepared_request.path_url, ) headers = { **endpoint_headers, 'Authorization': authorization_string, 'Date': date, 'Content-Type': content_type, } endpoint.prepared_request.body = content endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) endpoint.prepared_request.prepare_content_length(body=content) session = requests.Session() response = session.send( # type: ignore request=endpoint.prepared_request, ) url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc if netloc == 'cloudreco.vuforia.com': # The multipart/formdata boundary is no longer in the given # content. assert response.text == '' assert_vwq_failure( response=response, status_code=HTTPStatus.UNSUPPORTED_MEDIA_TYPE, content_type=None, ) return assert response.status_code == HTTPStatus.BAD_REQUEST assert response.text == '' assert 'Content-Type' not in response.headers
def query( self, image: io.BytesIO, max_num_results: int = 1, include_target_data: CloudRecoIncludeTargetData = ( CloudRecoIncludeTargetData.TOP ), ) -> List[QueryResult]: """ Use the Vuforia Web Query API to make an Image Recognition Query. See https://library.vuforia.com/articles/Solution/How-To-Perform-an-Image-Recognition-Query for parameter details. Args: image: The image to make a query against. max_num_results: The maximum number of matching targets to be returned. include_target_data: Indicates if target_data records shall be returned for the matched targets. Accepted values are top (default value, only return target_data for top ranked match), none (return no target_data), all (for all matched targets). Raises: ~vws.exceptions.cloud_reco_exceptions.AuthenticationFailure: The client access key pair is not correct. ~vws.exceptions.cloud_reco_exceptions.MaxNumResultsOutOfRange: ``max_num_results`` is not within the range (1, 50). ~vws.exceptions.cloud_reco_exceptions.MatchProcessing: The given image matches a target which was recently added, updated or deleted and Vuforia returns an error in this case. ~vws.exceptions.cloud_reco_exceptions.InactiveProject: The project is inactive. ~vws.exceptions.custom_exceptions.ConnectionErrorPossiblyImageTooLarge: The given image is too large. ~vws.exceptions.cloud_reco_exceptions.RequestTimeTooSkewed: There is an error with the time sent to Vuforia. ~vws.exceptions.cloud_reco_exceptions.BadImage: There is a problem with the given image. For example, it must be a JPEG or PNG file in the grayscale or RGB color space. Returns: An ordered list of target details of matching targets. """ image_content = image.getvalue() body = { 'image': ('image.jpeg', image_content, 'image/jpeg'), 'max_num_results': (None, int(max_num_results), 'text/plain'), 'include_target_data': ( None, include_target_data.value, 'text/plain', ), } date = rfc_1123_date() request_path = '/v1/query' content, content_type_header = encode_multipart_formdata(body) method = 'POST' authorization_string = authorization_header( access_key=self._client_access_key, secret_key=self._client_secret_key, method=method, content=content, # Note that this is not the actual Content-Type header value sent. content_type='multipart/form-data', date=date, request_path=request_path, ) headers = { 'Authorization': authorization_string, 'Date': date, 'Content-Type': content_type_header, } try: response = requests.request( method=method, url=urljoin(base=self._base_vwq_url, url=request_path), headers=headers, data=content, ) except requests.exceptions.ConnectionError as exc: raise ConnectionErrorPossiblyImageTooLarge( request=exc.request, response=exc.response, ) from exc if 'Integer out of range' in response.text: raise MaxNumResultsOutOfRange(response=response) if 'No content to map due to end-of-input' in response.text: raise MatchProcessing(response=response) result_code = response.json()['result_code'] if result_code != 'Success': exception = { 'AuthenticationFailure': AuthenticationFailure, 'BadImage': BadImage, 'InactiveProject': InactiveProject, 'RequestTimeTooSkewed': RequestTimeTooSkewed, }[result_code] raise exception(response=response) result = [] result_list = list(response.json()['results']) for item in result_list: target_data: Optional[TargetData] = None if 'target_data' in item: target_data_dict = item['target_data'] metadata = target_data_dict['application_metadata'] timestamp_string = target_data_dict['target_timestamp'] target_timestamp = datetime.datetime.utcfromtimestamp( timestamp_string, ) target_data = TargetData( name=target_data_dict['name'], application_metadata=metadata, target_timestamp=target_timestamp, ) query_result = QueryResult( target_id=item['target_id'], target_data=target_data, ) result.append(query_result) return result
def test_invalid_json( self, endpoint: Endpoint, date_skew_minutes: int, ) -> None: """ Giving invalid JSON to endpoints returns error responses. """ date_is_skewed = not date_skew_minutes == 0 content = b'a' gmt = ZoneInfo('GMT') now = datetime.now(tz=gmt) time_to_freeze = now + timedelta(minutes=date_skew_minutes) with freeze_time(time_to_freeze): date = rfc_1123_date() endpoint_headers = dict(endpoint.prepared_request.headers) authorization_string = authorization_header( access_key=endpoint.access_key, secret_key=endpoint.secret_key, method=str(endpoint.prepared_request.method), content=content, content_type=endpoint.auth_header_content_type, date=date, request_path=endpoint.prepared_request.path_url, ) headers = { **endpoint_headers, 'Authorization': authorization_string, 'Date': date, } endpoint.prepared_request.body = content endpoint.prepared_request.headers = CaseInsensitiveDict(data=headers) endpoint.prepared_request.prepare_content_length(body=content) session = requests.Session() response = session.send( # type: ignore request=endpoint.prepared_request, ) takes_json_data = ( endpoint.auth_header_content_type == 'application/json' ) assert_valid_date_header(response=response) if date_is_skewed and takes_json_data: # On the real implementation, we get `HTTPStatus.FORBIDDEN` and # `REQUEST_TIME_TOO_SKEWED`. # See https://github.com/VWS-Python/vws-python-mock/issues/4 for # implementing this on them mock. return if not date_is_skewed and takes_json_data: assert_vws_failure( response=response, status_code=HTTPStatus.BAD_REQUEST, result_code=ResultCodes.FAIL, ) return assert response.status_code == HTTPStatus.BAD_REQUEST url = str(endpoint.prepared_request.url) netloc = urlparse(url).netloc if netloc == 'cloudreco.vuforia.com': assert_vwq_failure( response=response, status_code=HTTPStatus.BAD_REQUEST, content_type='text/html;charset=UTF-8', ) expected_text = ( 'java.lang.RuntimeException: RESTEASY007500: ' 'Could find no Content-Disposition header within part' ) assert response.text == expected_text return assert response.text == '' assert 'Content-Type' not in response.headers