File: //snap/google-cloud-cli/394/lib/googlecloudsdk/appengine/tools/appengine_rpc_test_util.py
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for testing code that uses appengine_rpc's *RpcServer."""
from __future__ import absolute_import
import io
import logging
from googlecloudsdk.appengine.tools.appengine_rpc import AbstractRpcServer
from googlecloudsdk.appengine.tools.appengine_rpc import HttpRpcServer
from googlecloudsdk.appengine._internal import six_subset
# pylint:disable=g-import-not-at-top
# pylint:disable=invalid-name
# Inline these directly rather than placing in six_subset since importing
# urllib into six_subset seems to mess with the overridden version of
# urllib/httplib that the NaCl runtime sandbox inserts for SSL purposes.
if six_subset.PY3:
import urllib.error
HTTPError = urllib.error.HTTPError
else:
import urllib2
HTTPError = urllib2.HTTPError
# pylint:disable=g-import-not-at-top
# pylint:disable=invalid-name
class TestRpcServerMixin(object):
"""Provides a mocked-out version of HttpRpcServer for testing purposes."""
def set_strict(self, strict=True):
"""Enables strict mode."""
self.opener.set_strict(strict)
def set_save_request_data(self, save_request_data=True):
"""Enables saving request data for every request."""
self.opener.set_save_request_data(save_request_data)
def _GetOpener(self):
"""Returns a MockOpener.
Returns:
A MockOpener object.
"""
return TestRpcServerMixin.MockOpener()
class MockResponse(object):
"""A mocked out response object for testing purposes."""
def __init__(self, body, code=200, headers=None):
"""Creates a new MockResponse.
Args:
body: The text of the body to return.
code: The response code (default 200).
headers: An optional header dictionary.
"""
self.fp = io.BytesIO(body)
self.code = code
self.headers = headers
self.msg = ""
if self.headers is None:
self.headers = {}
def info(self):
return self.headers
def read(self, length=-1):
"""Reads from the response body.
Args:
length: The number of bytes to read.
Returns:
The body of the response.
"""
return self.fp.read(length)
def readline(self):
"""Reads a line from the response body.
Returns:
A line of text from the response body.
"""
return self.fp.readline()
def close(self):
"""Closes the response stream."""
self.fp.close()
class MockOpener(object):
"""A mocked-out OpenerDirector for testing purposes."""
def __init__(self):
"""Creates a new MockOpener."""
self.request_data = []
self.requests = []
self.responses = {}
self.ordered_responses = {}
self.cookie = None
self.strict = False
self.save_request_data = False
def set_strict(self, strict=True):
"""Enables strict mode."""
self.strict = strict
def set_save_request_data(self, save_request_data=True):
"""Enables saving request data for every request."""
self.save_request_data = save_request_data
def open(self, request):
"""Logs the request and returns a MockResponse object."""
full_url = request.get_full_url()
if "?" in full_url:
url = full_url[:full_url.find("?")]
else:
url = full_url
if (url != "https://www.google.com/accounts/ClientLogin"
and not url.endswith("_ah/login")):
assert "X-appcfg-api-version" in request.headers
assert "User-agent" in request.headers
request_data = (full_url, bool(request.data))
self.requests.append(request_data)
if self.save_request_data:
self.request_data.append((full_url, request.data))
if self.cookie:
request.headers["Cookie"] = self.cookie
response = self.responses[url](request)
# Use ordered responses in preference to specific response to generic 200.
if url in self.ordered_responses:
logging.debug("Using ordered pre-canned response for: %s" % full_url)
response = self.ordered_responses[url].pop(0)(request)
if not self.ordered_responses[url]:
self.ordered_responses.pop(url)
elif url in self.responses:
logging.debug("Using pre-canned response for: %s" % full_url)
response = self.responses[url](request)
elif self.strict:
raise Exception('No response found for url: %s (%s)' % (url, full_url))
else:
logging.debug("Using generic blank response for: %s" % full_url)
response = TestRpcServerMixin.MockResponse(b"")
if "Set-Cookie" in response.headers:
self.cookie = response.headers["Set-Cookie"]
# Handle error status codes in the same way as the appengine_rpc openers.
# urllib2 will raise HTTPError for non-2XX status codes, per RFC 2616.
if not (200 <= response.code < 300):
code, msg, hdrs = response.code, response.msg, response.info()
fp = io.BytesIO(response.read())
raise HTTPError(url=url, code=code, msg=None, hdrs=hdrs, fp=fp)
return response
def AddResponse(self, url, response_func):
"""Calls the provided function when the provided URL is requested.
The provided function should accept a request object and return a
response object.
Args:
url: The URL to trigger on.
response_func: The function to call when the url is requested.
"""
self.responses[url] = response_func
def AddOrderedResponse(self, url, response_func):
"""Calls the provided function when the provided URL is requested.
The provided functions should accept a request object and return a
response object. This response will be added after previously given
responses if they exist.
Args:
url: The URL to trigger on.
response_func: The function to call when the url is requested.
"""
if url not in self.ordered_responses:
self.ordered_responses[url] = []
self.ordered_responses[url].append(response_func)
def AddOrderedResponses(self, url, response_funcs):
"""Calls the provided function when the provided URL is requested.
The provided functions should accept a request object and return a
response object. Each response will be called once.
Args:
url: The URL to trigger on.
response_funcs: A list of response functions.
"""
self.ordered_responses[url] = response_funcs
class TestRpcServer(TestRpcServerMixin, AbstractRpcServer):
pass
class TestHttpRpcServer(TestRpcServerMixin, HttpRpcServer):
pass
class UrlLibRequestResponseStub(object):
def __init__(self, headers=None):
self.headers = {}
if headers:
self.headers = headers
def add_header(self, header, value):
# Note that this does not preserve header order.
# If that's a problem for your tests, add some functionality :)
self.headers[header] = value
class UrlLibRequestStub(UrlLibRequestResponseStub):
pass
class UrlLibResponseStub(UrlLibRequestResponseStub, io.BytesIO):
def __init__(self, body, headers, url, code, msg):
UrlLibRequestResponseStub.__init__(self, headers)
if body:
io.BytesIO.__init__(self, body)
else:
io.BytesIO.__init__(self, b"")
self.url = url
self.code = code
self.msg = msg