stubserver-1.1/0000777000000000000000000000000013230143172013515 5ustar rootroot00000000000000stubserver-1.1/PKG-INFO0000777000000000000000000000142613230143172014620 0ustar rootroot00000000000000Metadata-Version: 1.1 Name: stubserver Version: 1.1 Summary: A stub webserver used to enable blackbox testing of applications that call external web urls. For example, an application that consumes data from an external REST api. The usage pattern is intended to be very much like using a mock framework. Home-page: http://www.pyruby.com/pythonstubserver Author: Chris Tarttelin and Point 2 inc Author-email: chris@pyruby.co.uk License: FreeBSD Description-Content-Type: UNKNOWN Description: UNKNOWN Keywords: test,unittest,mock,http,ftp Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 stubserver-1.1/README.rst0000777000000000000000000000331713163437452015227 0ustar rootroot00000000000000=========== Stub Server =========== .. image:: https://api.travis-ci.org/tarttelin/Python-Stub-Server.svg :target: https://travis-ci.org/tarttelin/Python-Stub-Server .. image:: https://coveralls.io/repos/tarttelin/Python-Stub-Server/badge.svg?branch=master&service=github :target: https://coveralls.io/github/tarttelin/Python-Stub-Server?branch=master Testing external web dependencies in a mock objects style. Written for Python 2.6, ported to Python 2.5 this library includes the tests at the bottom of the stubserver.py file, which serve both as the TDD tests written while creating this library, and as examples / documentation. It supports any HTTP method, i.e. GET, PUT, POST and DELETE. It supports chunked encoding, but currently we have no use cases for multipart support etc, so it doesn't do it. An excerpt from the tests is below: :: from unittest import TestCase from stubserver import StubServer class WebTest(TestCase): def setUp(self): self.server = StubServer(8998) self.server.run() def tearDown(self): self.server.stop() # implicitly calls verify on stop def test_put_with_capture(self): capture = {} self.server.expect(method="PUT", url="/address/\d+$", data_capture=capture)\ .and_return(reply_code=201) # do stuff here captured = eval(capture["body"]) self.assertEquals("world", captured["hello"]) Though stubserver is at version 0.1, it is actively used in Python 2.5 to Python 2.7 codebases, so it is fairly bug free. There is also an FTPStubServer for your FTP testing needs, but that is NOT bug free at the moment. All assistance gratefully received. stubserver-1.1/setup.cfg0000777000000000000000000000011713230143172015340 0ustar rootroot00000000000000[metadata] description-file = README.md [egg_info] tag_build = tag_date = 0 stubserver-1.1/setup.py0000777000000000000000000000126713163437452015254 0ustar rootroot00000000000000#!/usr/bin/env python from setuptools import setup import stubserver setup(name='stubserver', version=stubserver.__version__, description=stubserver.__doc__, author=stubserver.__author__, author_email=stubserver.__email__, url=stubserver.__url__, packages=['stubserver'], licence='FreeBSD', test_suite='test', keywords=['test', 'unittest', 'mock', 'http', 'ftp'], classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', ] ) stubserver-1.1/stubserver/0000777000000000000000000000000013230143172015721 5ustar rootroot00000000000000stubserver-1.1/stubserver/ftpserver.py0000777000000000000000000001331613163437452020336 0ustar rootroot00000000000000import threading import sys if sys.version_info[0] < 3: import SocketServer else: import socketserver as SocketServer class FTPServer(SocketServer.BaseRequestHandler): def __init__(self, hostname, port, interactions, files): self.hostname = hostname self.port = port self.interactions = interactions self.files = files self.cwd = '/' def __call__(self, request, client_address, server): self.request = request self.client_address = client_address self.server = server self.setup() try: self.handle() finally: self.finish() return self def handle(self): # Establish connection self.request.send(b'220 (FtpStubServer 0.1a)\r\n') self.communicating = True while self.communicating: cmd = self.request.recv(1024) if len(cmd) == 0: break if cmd: self.interactions.append(cmd) cmd = cmd.decode('utf-8').rstrip() first = cmd.split(' ', 1)[0] getattr(self, '_' + first)(cmd) def _USER(self, cmd): self.request.send(b'331 Please specify password.\r\n') def _PASS(self, cmd): self.request.send(b'230 You are now logged in.\r\n') def _TYPE(self, cmd): self.request.send(b'200 Switching to ascii mode.\r\n') def _PASV(self, cmd): self.data_handler = FTPDataServer(self.files) self.port += 1 SocketServer.TCPServer.allow_reuse_address = True self.data_server = SocketServer.TCPServer((self.hostname, self.port + 1), self.data_handler) self.request.send(('227 Entering Passive Mode. (127,0,0,1,%s,%s)\r\n' % ( int((self.port + 1) / 256), (self.port + 1) % 256)).encode('utf-8')) def child_go(self, action): self.data_handler.set_action(action) self.data_server.handle_request() self.data_server.server_close() def _STOR(self, cmd): filename = cmd.split(' ', 2)[1] self.data_handler.set_filename(filename) self.request.send(b'150 Okay to send data\r\n') self.child_go('STOR') self.request.send(b'226 Got the file\r\n') def _LIST(self, cmd): self.request.send(b'150 Accepted data connection\r\n') self.child_go('LIST') self.request.send(b'226 You got the listings now\r\n') def _RETR(self, cmd): filename = cmd.split(' ', 2)[1] self.data_handler.set_filename(filename) self.request.send(b'150 Accepted data connection\r\n') self.child_go('RETR') self.request.send(b'226 Enjoy your file\r\n') def _CWD(self, cmd): self.cwd = cmd.split(' ', 2)[1] self.request.send(('250 OK. Current directory is "%s"\r\n' % self.cwd).encode('utf-8')) def _PWD(self, cmd): self.request.send(('257 "%s" is your current location\r\n' % self.cwd).encode('utf-8')) def _MKD(self, cmd): mkd = cmd.split(' ', 2)[1] self.request.send(('257 "%s" folder created\r\n' % mkd).encode('utf-8')) def _NLST(self, cmd): self.request.send(b'150 Accepted data connection\r\n') self.child_go('NLST') self.request.send(b'226 You got the listings now\r\n') def _QUIT(self, cmd): self.communicating = False self.request.send(b'221-Goodbye.\r\n221 Have fun.') class FTPDataServer(SocketServer.StreamRequestHandler): def __init__(self, files): self.files = files self.command = 'LIST' def __call__(self, request, client_address, server): self.request = request self.client_address = client_address self.server = server self.setup() try: self.handle() return self finally: self.finish() def set_action(self, action): self.action = action def handle(self): getattr(self, '_' + self.action)() def set_filename(self, filename): self.filename = filename.encode('utf-8') def _STOR(self): self.files[self.filename] = self.rfile.read().strip() def _LIST(self): data = b'\n'.join([name for name in self.files.keys()]) self.wfile.write(data) def _NLST(self): data = b'\015\012'.join([name for name in self.files.keys()]) self.wfile.write(data) def _RETR(self): self.wfile.write(self.files[self.filename]) class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): pass class FTPStubServer(object): def __init__(self, port, hostname='localhost'): self.hostname = hostname self.port = port self._interactions = [] self._files = {} def files(self, name): name = name.encode('utf-8') if name in self._files: return self._files[name].decode('utf-8') return None def add_file(self, name, content): self._files[name.encode('utf-8')] = content.encode('utf-8') def run(self, timeout=2): self.handler = FTPServer(self.hostname, self.port, self._interactions, self._files) self.server = ThreadedTCPServer((self.hostname, self.port), self.handler) # Retrieving actual port when using a random one. if self.port == 0: self.port = self.server.server_address[1] self.handler.port = self.port server_thread = threading.Thread(target=self.server.serve_forever) # Exit the server thread when the main thread terminates server_thread.daemon = True server_thread.start() def stop(self): self.server.shutdown() while self._interactions: self._interactions.pop() while self._files: self._files.popitem() stubserver-1.1/stubserver/webserver.py0000777000000000000000000002166713230142307020315 0ustar rootroot00000000000000import sys import threading import re import time if sys.version_info[0] < 3: import BaseHTTPServer else: import http.server as BaseHTTPServer if sys.version_info[0] < 3: from urllib import urlopen else: from urllib.request import urlopen class StoppableHTTPServer(BaseHTTPServer.HTTPServer): """ Python 2.5 HTTPServer does not close down properly when calling server_close. The implementation below was based on the comments in the below article: http://stackoverflow.com/questions/268629/how-to-stop-basehttpserver-serveforever-in-a-basehttprequesthandler-subclass """ stopped = False allow_reuse_address = True def __init__(self, *args, **kw): BaseHTTPServer.HTTPServer.__init__(self, *args, **kw) def serve_forever(self): while not self.stopped: self.handle_request() def server_close(self): BaseHTTPServer.HTTPServer.server_close(self) self.stopped = True self._create_dummy_request() time.sleep(0.5) def shutdown(self): pass def _create_dummy_request(self): f = urllib.urlopen("http://localhost:" + str(self.server_port) + "/__shutdown") f.read() f.close() if sys.version_info < (2, 6): HTTPServer = StoppableHTTPServer else: HTTPServer = BaseHTTPServer.HTTPServer class StubServer(object): def __init__(self, port=8080, address='localhost'): self._expectations = [] self.port = port self.address = address def run(self): server_address = (self.address, self.port) self.httpd = HTTPServer(server_address, StubResponse(self._expectations)) thread = threading.Thread(target=self._run) thread.start() def stop(self): self.httpd.shutdown() self.httpd.server_close() self.verify() def _run(self): try: self.httpd.serve_forever() except: pass def verify(self): """ Check all exceptation has been made. :raises: Exception: If one them isn't made. """ failures = [] for expectation in self._expectations: if not expectation.satisfied: failures.append(str(expectation)) del self._expectations[:] if failures: raise Exception("Unsatisfied expectations: " + "\n".join(failures)) def expect(self, method="GET", url="^UrlRegExpMather$", data=None, data_capture=None, file_content=None): """ Prepare :class:`StubServer` to handle an HTTP request. :param method: HTTP method :type method: ``str`` :param url: Regex matching with path part of an URL :type url: Raw ``str`` :param data: Excepted data :type data: ``None`` or other :param data_capture: Dictionary given by user for gather data returned by server. :type data_capture: ``dict`` :param file_content: Unsed :return: Expectation object initilized :rtype: :class:`Expectation` """ expected = Expectation(method, url, data, data_capture) self._expectations.append(expected) return expected class Expectation(object): def __init__(self, method, url, data, data_capture): """ :param method: HTTP method :type method: ``str`` :param url: Regex matching with path part of an URL :type url: ``str`` :param data: Excepted data :type data: ``None`` or other :param data_capture: Dictionary given by user for gather data returned by server. :type data_capture: ``dict`` """ if data_capture is None: data_capture = {} self.method = method self.url = url self.data = data self.data_capture = data_capture self.satisfied = False def and_return(self, mime_type="text/html", reply_code=200, content="", file_content=None, headers=None): """ Define the response created by the expectation. :param mime_type: Define content type of HTTP response :type mime_type: ``str`` :param reply_code: Define response code of HTTP response :type reply_code: ``int`` :param content: Define response's content :type content: ``str`` :param file_content: Define response's content from a file :type file_content: ``str`` :param headers: Additional HTTP header fields to be sent :type headers: ``iterable of tuples (header field name, value)`` """ if file_content: f = open(file_content, "r") content = f.read() f.close() self.response = (reply_code, mime_type, content, headers) def __str__(self): return "%s %s \n data_capture: %s\n" % (self.method, self.url, self.data_capture) class StubResponse(BaseHTTPServer.BaseHTTPRequestHandler): def __call__(self, request, client_address, server): self.request = request self.client_address = client_address self.server = server try: self.setup() self.handle() finally: self.finish() def __init__(self, expectations): self.expected = expectations def _get_data(self): max_chunk_size = 10 * 1024 * 1024 if "content-length" not in self.headers: return b'' size_remaining = int(self.headers["content-length"]) data = [] while size_remaining: chunk_size = min(size_remaining, max_chunk_size) data.append(self.rfile.read(chunk_size)) size_remaining -= len(data[-1]) return b''.join(data) def handle_one_request(self): """Handle a single HTTP request. You normally don't need to override this method; see the class __doc__ string for information on how to handle specific HTTP commands such as GET and POST. """ def send_headers(exp): self.send_header("Content-Type", exp.response[1]) headers = exp.response[3] if headers: for header in headers: self.send_header(header[0], header[1]) self.end_headers() self.raw_requestline = self.rfile.readline() if not self.raw_requestline: self.close_connection = 1 return if not self.parse_request(): # An error code has been sent, just exit return method = self.command if self.path == "/__shutdown": self.send_response(200, "Python") data = self._get_data().decode('utf-8') expectations_matching_url = [x for x in self.expected if re.search(x.url, self.path)] expectations_matching_method = [x for x in expectations_matching_url if x.method == method] matching_expectations = [x for x in expectations_matching_method if not x.satisfied] matching_expectations_with_data = [x for x in matching_expectations if x.data and x.data == data] err_code = err_message = err_body = None if len(matching_expectations_with_data) > 0: exp = matching_expectations_with_data[0] self.send_response(exp.response[0], "Python") send_headers(exp) self.wfile.write(exp.response[2].encode('utf-8')) exp.satisfied = True exp.data_capture["body"] = data elif len(matching_expectations) > 0: exp = matching_expectations[0] if exp.data: err_code = 403 err_message = "Payload missing or incorrect" err_body = "This URL expects data: {0}. Query provided: {1}".format(exp.data, data) else: self.send_response(exp.response[0], "Python") send_headers(exp) self.wfile.write(exp.response[2].encode('utf-8')) exp.satisfied = True exp.data_capture["body"] = data elif len(expectations_matching_method) > 0: # All expectations have been fulfilled err_code = 400 err_message = "Expectations exhausted" err_body = "Expectations at this URL have already been satisfied.\n" + str(expectations_matching_method) elif len(expectations_matching_url) > 0: # Method not allowed err_code = 405 err_message = "Method not allowed" err_body = "Method " + method + " not allowed.\n" + str(expectations_matching_url) else: # not found err_code = 404 err_message = "Not found" err_body = "No URL pattern matched." if err_code is not None: self.send_response(err_code, err_message) self.send_header("Content-Type", "text/plain") self.end_headers() self.wfile.write(err_body.encode('utf-8')) self.wfile.flush() def log_request(code=None, size=None): pass stubserver-1.1/stubserver/__init__.py0000777000000000000000000000076613230142465020052 0ustar rootroot00000000000000"""A stub webserver used to enable blackbox testing of applications that call external web urls. For example, an application that consumes data from an external REST api. The usage pattern is intended to be very much like using a mock framework.""" from stubserver.webserver import StubServer from stubserver.ftpserver import FTPStubServer VERSION = __version__ = '1.1' __author__ = 'Chris Tarttelin and Point 2 inc' __email__ = 'chris@pyruby.co.uk' __url__ = 'http://www.pyruby.com/pythonstubserver' stubserver-1.1/stubserver.egg-info/0000777000000000000000000000000013230143172017413 5ustar rootroot00000000000000stubserver-1.1/stubserver.egg-info/dependency_links.txt0000777000000000000000000000000113230143172023464 0ustar rootroot00000000000000 stubserver-1.1/stubserver.egg-info/PKG-INFO0000777000000000000000000000142613230143172020516 0ustar rootroot00000000000000Metadata-Version: 1.1 Name: stubserver Version: 1.1 Summary: A stub webserver used to enable blackbox testing of applications that call external web urls. For example, an application that consumes data from an external REST api. The usage pattern is intended to be very much like using a mock framework. Home-page: http://www.pyruby.com/pythonstubserver Author: Chris Tarttelin and Point 2 inc Author-email: chris@pyruby.co.uk License: FreeBSD Description-Content-Type: UNKNOWN Description: UNKNOWN Keywords: test,unittest,mock,http,ftp Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 stubserver-1.1/stubserver.egg-info/SOURCES.txt0000777000000000000000000000035413230143172021304 0ustar rootroot00000000000000README.rst setup.cfg setup.py stubserver/__init__.py stubserver/ftpserver.py stubserver/webserver.py stubserver.egg-info/PKG-INFO stubserver.egg-info/SOURCES.txt stubserver.egg-info/dependency_links.txt stubserver.egg-info/top_level.txtstubserver-1.1/stubserver.egg-info/top_level.txt0000777000000000000000000000001313230143172022142 0ustar rootroot00000000000000stubserver