From 16d1463bf146f11eb45dd1d6774f248f1257c6ad Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Fri, 27 Jan 2017 12:57:37 -0500 Subject: [PATCH] client, transport: implement ability to send raw requests Introduce a new method to allow tests to explicitly inspect the status code and complete body of HTTP responses received when issuing commands. In addition, extend the behavior of the higher-order `send_response` method with assertions for HTTP response headers. --- webdriver/client.py | 47 +++++++++++++++++++++++++--- webdriver/transport.py | 69 ++++++++++++++++++++++++++++-------------- 2 files changed, 88 insertions(+), 28 deletions(-) diff --git a/webdriver/client.py b/webdriver/client.py index b7d37b92545581..fd0c6e6a43383f 100644 --- a/webdriver/client.py +++ b/webdriver/client.py @@ -215,13 +215,13 @@ def start(self): #body["capabilities"] = caps body = caps - resp = self.transport.send("POST", "session", body=body) - self.session_id = resp["sessionId"] + response = self.transport.send("POST", "session", body=body) + self.session_id = response.body["value"]["sessionId"] if self.extension_cls: self.extension = self.extension_cls(self) - return resp["value"] + return response.body["value"] def end(self): if self.session_id is None: @@ -236,11 +236,48 @@ def end(self): self.find = None self.extension = None + def send_raw_command(self, method, url, body=None, headers=None): + """Send a command to the remote end. + + :param method: HTTP method to use in request + :param url: "command part" of the requests URL path + :param body: body of the HTTP request + :param headers: Additional headers to include in the HTTP request + + :return: an instance of wdclient.Response describing the HTTP response + received from the remote end + """ + url = urlparse.urljoin("session/%s/" % self.session_id, url) + return self.transport.send(method, url, body, headers) + def send_command(self, method, url, body=None, key=None): + """Send a command to the remote end and validate its success. + + :param method: HTTP method to use in request + :param url: "command part" of the requests URL path + :param body: body of the HTTP request + :param key: (deprecated) when specified, this string value will be used + to de-reference the HTTP response body following JSON parsing + + :return: None if the HTTP response body was empty, otherwise the + result of parsing the HTTP response body as JSON + """ + if self.session_id is None: raise error.SessionNotCreatedException() - url = urlparse.urljoin("session/%s/" % self.session_id, url) - return self.transport.send(method, url, body, key=key) + + response = self.send_raw_command(method, url, body) + + if response.status != 200: + cls = error.get(response.body["value"].get("error")) + raise cls(response.body["value"].get("message")) + + if key is not None: + response.body = response.body[key] + if not response.body: + response.body = None + + return response.body @property @command diff --git a/webdriver/transport.py b/webdriver/transport.py index f3c1ed536f07b7..db63467bf6558b 100644 --- a/webdriver/transport.py +++ b/webdriver/transport.py @@ -6,11 +6,46 @@ import json import urlparse -import error - - HTTP_TIMEOUT = 5 +class Response(object): + """Describes an HTTP response received from a remote en"Describes an HTTP + response received from a remote end whose body has been read and parsed as + appropriate.""" + def __init__(self, status, body): + self.status = status + self.body = body + + def __repr__(self): + return "wdclient.Response(status=%d, body=%s)" % (self.status, self.body) + + @staticmethod + def from_http_response(http_response): + status = http_response.status + body = http_response.read() + + # SpecID: dfn-send-a-response + # + # > 3. Set the response's header with name and value with the following + # > values: + # > + # > "Content-Type" + # > "application/json; charset=utf-8" + # > "cache-control" + # > "no-cache" + assert http_response.getheader("Content-Type") == "application/json; charset=utf-8" + assert http_response.getheader("Cache-Control") == "no-cache" + + if body: + body = json.loads(body) + + # SpecID: dfn-send-a-response + # + # > 4. If data is not null, let response's body be a JSON Object + # with a key `value` set to the JSON Serialization of data. + assert "value" in body + + return Response(status, body) class HTTPWireProtocol(object): """Transports messages (commands and responses) over the WebDriver @@ -33,15 +68,16 @@ def __init__(self, host, port, url_prefix="/", timeout=HTTP_TIMEOUT): def url(self, suffix): return urlparse.urljoin(self.path_prefix, suffix) - def send(self, method, url, body=None, headers=None, key=None): + def send(self, method, url, body=None, headers=None): """Send a command to the remote. :param method: "POST" or "GET". + :param url: "command part" of the requests URL path :param body: Body of the request. Defaults to an empty dictionary if ``method`` is "POST". :param headers: Additional headers to include in the request. - :param key: Extract this key from the dictionary returned from - the remote. + :return: an instance of wdclient.Response describing the HTTP response + received from the remote end. """ if body is None and method == "POST": @@ -62,22 +98,9 @@ def send(self, method, url, body=None, headers=None, key=None): self.host, self.port, strict=True, timeout=self._timeout) conn.request(method, url, body, headers) - resp = conn.getresponse() - resp_body = resp.read() - conn.close() - try: - data = json.loads(resp_body) - except: - raise IOError("Could not parse response body as JSON: '%s'" % resp_body) - - if resp.status != 200: - cls = error.get(data.get("error")) - raise cls(data.get("message")) - - if key is not None: - data = data[key] - if not data: - data = None + response = Response.from_http_response(conn.getresponse()) + finally: + conn.close() - return data + return response