Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
*.pyc
*~
*.swp
.pytest_cache/
.vscode/
.tox/
/.idea/
/Results/
/Tests/Results/
/build/
/dist/
/robotframework_zeeplibrary.egg-info
185 changes: 104 additions & 81 deletions ZeepLibrary/zeeplibrary.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,74 @@
import os
import zeep
import requests
import requests.auth
from requests import Session
from robot.api import logger
from robot.api.deco import keyword
from lxml import etree
from zeep import Client
from zeep.transports import Transport
"""Library to provide Robot Framework keywords for testing SOAP services.
Wraps the python zeep module's functionalities to accomplish this.
"""
import base64
import mimetypes
import os

from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.image import MIMEImage
from email.encoders import encode_7or8bit, encode_base64, encode_noop
import base64
# from email.encoders import encode_7or8bit, encode_base64, encode_noop
from email.encoders import encode_7or8bit, encode_noop

import requests
import requests.auth
import zeep

from lxml import etree
# from requests import Session
from robot.api.deco import keyword
from robot.api import logger
# from zeep import Client
# from zeep.transports import Transport


class ZeepLibraryException(Exception):
"""Custom base class for ZeepLibrary exceptions.
New exceptions should base this and overwrite the err_msg attribute.
"""
def __init__(self):
self.err_msg = "Default error message"

def __str__(self):
return self.message
return self.err_msg


class AliasAlreadyInUseException(ZeepLibraryException):
"""Raise when an alias is already in use."""
def __init__(self, alias):
self.message = "The alias `{}' is already in use.".format(alias)
self.err_msg = "The alias '{}' is already in use.".format(alias)


class ClientNotFoundException(ZeepLibraryException):
"""Raise when a client could not be found with given alias."""
def __init__(self, alias):
self.message = "Could not find a client with alias `{}'."\
.format(alias)
self.err_msg = "Could not find a client with alias '{}'.".format(alias)


class AliasNotFoundException(ZeepLibraryException):
"""Raise when an alias could not be found for a client."""
def __init__(self):
self.message = "Could not find alias for the provided client."
self.err_msg = "Could not find alias for the provided client."


class AliasRequiredException(ZeepLibraryException):
"""Raise when there is more than one client but no alias was specified."""
def __init__(self):
self.message = ("When using more than one client, providing an alias "
self.err_msg = ("When using more than one client, providing an alias "
"is required.")


class ZeepLibrary:
class ZeepLibrary(object):
"""This library is built on top of the library Zeep in order to bring its
functionality to Robot Framework. Following in the footsteps of
the (now unmaintained) SudsLibrary, it allows testing SOAP
communication. Zeep offers a more intuitive and modern approach than
Suds does, and especially since the latter is unmaintained now, it
seemed time to write a library to enable Robot Framework to use Zeep.
Note: inheriting from 'object' makes ZeepLibrary a new style class in py2.
"""

__version__ = '0.9.2'
Expand Down Expand Up @@ -161,8 +178,8 @@ def add_attachment(self,
else:
file_mode = 'rt'

with open(filepath, file_mode) as f:
contents = f.read()
with open(filepath, file_mode) as file_object:
contents = file_object.read()

attachment = {
'filename': filename,
Expand All @@ -183,25 +200,22 @@ def call_operation(self, operation, xop=False, **kwargs):
# xop=xop)
# return original_post_method(address, body, headers)
def post_with_attachments(address, message, headers):
message = self.create_message(operation, **kwargs)
logger.debug("HTTP Post to {}:\n".format(address))

headers, body = self._build_multipart_request(message,
xop=xop)

response = self.active_client.transport.session.post(address,
data=body,
headers=headers,
timeout=self.active_client.transport.operation_timeout)

logger.debug(_prettify_request(response.request))
logger.debug("HTTP Response from {0} (status: {1}):\n".format(address, response.status_code))
logger.debug(_prettify_response(response))

return response
message = self.create_message(operation, **kwargs)
logger.debug("HTTP Post to {}:\n".format(address))
headers, body = self._build_multipart_request(message, xop=xop)
response = self.active_client.transport.session.post(
address,
data=body,
headers=headers,
timeout=self.active_client.transport.operation_timeout
)
logger.debug(_prettify_request(response.request))
logger.debug("HTTP Response from {0} (status: {1}):\n"
.format(address, response.status_code))
logger.debug(_prettify_response(response))
return response
self.active_client.transport.post = post_with_attachments


operation_method = getattr(self.active_client.service, operation)
return operation_method(**kwargs)

Expand All @@ -217,7 +231,12 @@ def close_client(self, alias=None):

@keyword('Close all clients')
def close_all_clients(self):
for alias in self.clients.keys():
# FIX
# old code produced error: dictionary changed size during iteration
# for alias in self.clients.keys():
# self.close_client(alias)
aliases = list(self.clients.keys())
for alias in aliases:
self.close_client(alias)

def _add_client(self, client, alias=None):
Expand Down Expand Up @@ -255,21 +274,21 @@ def create_message(self, operation, to_string=True, **kwargs):
operation,
**kwargs)
if to_string:
return etree.tostring(message)
else:
return message
# etree.tostring returns byte object without encoding kw arg.
message = etree.tostring(message, encoding='unicode')
return message

@keyword('Create object')
def create_object(self, type, *args, **kwargs):
type_ = self.active_client.get_type(type)
def create_object(self, type_to_get, *args, **kwargs):
type_ = self.active_client.get_type(type_to_get)
return type_(*args, **kwargs)

@keyword('Get alias')
def get_alias(self, client=None):
if not client:
return self.active_client_alias
else:
for alias, client_ in self.clients.iteritems():
for alias, client_ in self.clients.items():
if client_ == client:
return alias
raise AliasNotFoundException()
Expand All @@ -281,19 +300,21 @@ def get_client(self, alias=None):
If no ``alias`` is provided, the active client will be assumed.
"""
if alias:
return self.clients[alias]
client = self.clients[alias]
else:
return self.active_client
client = self.active_client
return client

@keyword('Get clients')
def get_clients(self):
return self.clients

@keyword('Get namespace prefix')
def get_namespace_prefix_for_uri(self, uri):
for prefix, uri_ in self.active_client.namespaces.iteritems():
for prefix, uri_ in self.active_client.namespaces.items():
if uri == uri_:
return prefix
return None

@keyword('Get namespace URI')
def get_namespace_uri_by_prefix(self, prefix):
Expand Down Expand Up @@ -338,13 +359,14 @@ def _log(item, to_log=True, to_console=False):
elif to_console:
logger.console(item)


def _perform_xop_magic(message):
doc = etree.fromstring(message)

for element in doc.iter():
if (element.text and
len(element.text) > 0 and
len(element.text) % 4 == 0):
len(element.text) > 0 and
len(element.text) % 4 == 0):
try:
decoded_val = base64.b64decode(element.text)
if decoded_val.startswith('cid:'):
Expand All @@ -356,43 +378,44 @@ def _perform_xop_magic(message):
except TypeError:
continue

message = etree.tostring(doc)
message = etree.tostring(doc, encoding='unicode')
return message


def _prettify_request(request, hide_auth=True):
"""Pretty prints the request for the supplied `requests.Request`
object. Especially useful after having performed the request, in
order to inspect what was truly sent. To access the used request
on the `requests.Response` object use the `request` attribute.
"""
if hide_auth:
logger.warn(("Hiding the `Authorization' header for security "
"reasons. If you wish to display it anyways, pass "
"`hide_auth=False`."))
result = ('{}\n{}\n{}\n\n{}{}'.format(
'----------- REQUEST BEGIN -----------',
request.method + ' ' + request.url,
'\n'.join('{}: {}'.format(key, value)
for key, value in request.headers.items()
if not(key == 'Authorization' and hide_auth)),
request.body,
"\n"
'------------ REQUEST END ------------'
))
return result
"""Pretty prints the request for the supplied `requests.Request`
object. Especially useful after having performed the request, in
order to inspect what was truly sent. To access the used request
on the `requests.Response` object use the `request` attribute.
"""
if hide_auth:
logger.warn(("Hiding the `Authorization' header for security "
"reasons. If you wish to display it anyways, pass "
"`hide_auth=False`."))
result = ('{}\n{}\n{}\n\n{}{}'.format(
'----------- REQUEST BEGIN -----------',
request.method + ' ' + request.url,
'\n'.join('{}: {}'.format(key, value)
for key, value in request.headers.items()
if not(key == 'Authorization' and hide_auth)),
request.body,
"\n"
'------------ REQUEST END ------------'
))
return result


def _prettify_response(reponse):
result = ('{}\n{}\n{}\n\n{}{}'.format(
'----------- RESPONSE BEGIN -----------',
reponse.url,
'\n'.join('{}: {}'.format(key, value)
for key, value in reponse.headers.items()),
reponse.text,
"\n"
'------------ RESPONSE END ------------'
))
return result
result = ('{}\n{}\n{}\n\n{}{}'.format(
'----------- RESPONSE BEGIN -----------',
reponse.url,
'\n'.join('{}: {}'.format(key, value)
for key, value in reponse.headers.items()),
reponse.text,
"\n"
'------------ RESPONSE END ------------'
))
return result


def _add_or_replace_http_header_if_passed(mime_object, headers, key):
Expand Down
7 changes: 7 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
lxml
mock
mockito
pytest
pytest-mockito
robotframework>=3.2
zeep>=2.5.0
Empty file added tests/__init__.py
Empty file.
4 changes: 2 additions & 2 deletions tests/regression_set.robot
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ Namespace trickery
Log namespace prefix map

${client}= Get client
${some prefix}= Set variable ${client.namespaces.keys()[0]}
${some prefix}= Evaluate list(${client.namespaces})[0]
${uri}= Get namespace URI ${some prefix}

Should start with ${uri} http

${some uri}= Set variable ${client.namespaces.items()[0][1]}
${some uri}= Set Variable ${{ list(${client.namespaces}.values()) }}[0]
${prefix}= Get namespace prefix ${some uri}

Close client
Expand Down
10 changes: 10 additions & 0 deletions tests/sample.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<xml xmlversion="1.0" encoding="utf-8">
<element first_attr="attrvalue">
<subelement>
This is as far as it gets.
</subelement>
<subelement>
Or maybe this.
</subelement>
</element>
</xml>
Loading