[Osaas-commits] r2 - in trunk: . server server/osaas server/osaas/http server/test

scm-commit@wald.intevation.org scm-commit at wald.intevation.org
Mon Aug 27 13:09:42 CEST 2007


Author: bh
Date: 2007-08-27 13:09:41 +0200 (Mon, 27 Aug 2007)
New Revision: 2

Added:
   trunk/client/
   trunk/server/
   trunk/server/adduser.py
   trunk/server/osaas/
   trunk/server/osaas/__init__.py
   trunk/server/osaas/config.py
   trunk/server/osaas/dbbackend.py
   trunk/server/osaas/formparser.py
   trunk/server/osaas/http/
   trunk/server/osaas/http/__init__.py
   trunk/server/osaas/http/httpserver.py
   trunk/server/osaas/http/run.py
   trunk/server/osaas/http/threadedserver.py
   trunk/server/osaas/http/threadpool.py
   trunk/server/osaas/pycompat.py
   trunk/server/osaas/recordfilter.py
   trunk/server/osaas/run.py
   trunk/server/osaas/server.py
   trunk/server/osaas/userdb.py
   trunk/server/startosaas.py
   trunk/server/test/
   trunk/server/test/runtests.py
   trunk/server/test/serversupport.py
   trunk/server/test/support.py
   trunk/server/test/test_config.py
   trunk/server/test/test_formparser.py
   trunk/server/test/test_httpserver.py
   trunk/server/test/test_osasserver.py
   trunk/server/test/test_run.py
   trunk/server/test/test_threadedserver.py
   trunk/server/test/test_threadpool.py
   trunk/server/test/test_userdb.py
Log:
import osaas server

Added: trunk/server/adduser.py
===================================================================
--- trunk/server/adduser.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/adduser.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,49 @@
+#! /usr/bin/env python
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Adds a username and password to a OSAAS user database
+Usage
+  adduser.py -f USERDBFILE USERNAME
+"""
+
+import sys
+import getpass
+
+import optparse
+
+from osaas.userdb import UserDB
+
+def ask_password(username):
+    while 1:
+        pw1 = getpass.getpass("Password for %r: " % username)
+        pw2 = getpass.getpass("Repeat password: ")
+        if pw1 == pw2:
+            return pw1
+        else:
+            print "Passwords don't match."
+
+def main():
+    parser = optparse.OptionParser()
+    parser.add_option("--userdb", "-f")
+
+    opts, rest = parser.parse_args()
+
+    if len(rest) != 1:
+        print >>sys.stderr, __doc__
+        sys.exit(1)
+
+    username = rest[0]
+
+    userdb = UserDB()
+    userdb.read(opts.userdb)
+    userdb.add_user(username, ask_password(username))
+    userdb.write(opts.userdb)
+
+
+if __name__ == "__main__":
+    main()


Property changes on: trunk/server/adduser.py
___________________________________________________________________
Name: svn:executable
   + *
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/__init__.py
===================================================================
--- trunk/server/osaas/__init__.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/__init__.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,8 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+import pycompat


Property changes on: trunk/server/osaas/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/config.py
===================================================================
--- trunk/server/osaas/config.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/config.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,45 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+
+import optparse
+from xml.dom.minidom import parse
+
+class OSAASOption(optparse.Option):
+
+    ATTRS = optparse.Option.ATTRS[:]
+    ATTRS.extend(["xml_path"])
+
+
+class ConfigXMLPathError(Exception):
+
+    pass
+
+
+def find_xml_path(node, path):
+    for name in path.split("/"):
+        children = [child for child in node.childNodes
+                          if child.nodeName == name]
+        if not children:
+            return None
+        if len(children) > 1:
+            raise ConfigXMLPathError("More than one element matches %r" % path)
+        node = children[0]
+    node.normalize()
+    value_node = node.firstChild
+    if value_node.nodeType != value_node.TEXT_NODE:
+        raise ConfigXMLPathError("Config file entry %r must only have text"
+                                 " content" % path)
+    return value_node.data
+
+def read_config_file(filename, options, setter):
+    dom = parse(filename)
+    for option in options:
+        if option.dest is not None and option.xml_path:
+            value = find_xml_path(dom, option.xml_path)
+            if value is not None:
+                setter(option.dest, option.check_value(option.xml_path, value))


Property changes on: trunk/server/osaas/config.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/dbbackend.py
===================================================================
--- trunk/server/osaas/dbbackend.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/dbbackend.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,11 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Database backend of OSAAS"""
+
+def dbhandler(record):
+    pass


Property changes on: trunk/server/osaas/dbbackend.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/formparser.py
===================================================================
--- trunk/server/osaas/formparser.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/formparser.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,77 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Parse the OSAAS form data into a record"""
+
+import cgi
+
+class FormParserException(Exception):
+
+    pass
+
+
+class Field(object):
+
+    def __init__(self, name, required=False):
+        self.name = name
+        self.required = required
+
+
+class Record:
+
+    def set_fields(self, fields, formdata, fieldname_normalizer=None):
+        try:
+            parsed_form = cgi.parse_qs(formdata, keep_blank_values=True,
+                                       strict_parsing=True)
+        except ValueError, err:
+            raise FormParserException("Error parsing form data: %s" % err)
+
+        if fieldname_normalizer is not None:
+            for name, value in parsed_form.items():
+                normalized = fieldname_normalizer(name)
+                del parsed_form[name]
+                parsed_form[normalized] = value
+
+        for field in fields:
+            value = parsed_form.get(field.name)
+            if value:
+                if len(value) == 1:
+                    setattr(self, field.name, value[0])
+                else:
+                    raise FormParserException("only one value allowed"
+                                              " for field %r" % field.name)
+            elif field.required:
+                raise FormParserException("formdata is missing required"
+                                          " field %r" % field.name)
+
+form_fields = [Field(name, required=True)
+               for name in ["user", "responsetime",
+                            "wmsidextern", "wmsidintern",
+                            "requeststring"]]
+
+request_fields = [
+    Field("VERSION", required=True),
+    Field("REQUEST", required=True),
+    Field("WIDTH", required=True),
+    Field("HEIGHT", required=True),
+    Field("BBOX", required=True),
+    Field("LAYERS", required=True),
+    Field("SERVICE", required=True),
+    Field("FORMAT"),
+    Field("TRANSPARENT"),
+    Field("EXCEPTIONS"),
+    Field("BGCOLOR"),
+    Field("STYLES"),
+    Field("SRS"),
+    ]
+
+def parse_formdata(formdata):
+    record = Record()
+    record.set_fields(form_fields, formdata)
+    record.set_fields(request_fields, record.requeststring,
+                      fieldname_normalizer=lambda s: s.upper())
+    return record


Property changes on: trunk/server/osaas/formparser.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/http/__init__.py
===================================================================


Property changes on: trunk/server/osaas/http/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/http/httpserver.py
===================================================================
--- trunk/server/osaas/http/httpserver.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/http/httpserver.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,119 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""HTTP server that can run in a thread and be stopped"""
+
+import socket
+import errno
+import threading
+import BaseHTTPServer
+import logging
+
+
+class HTTPServer(BaseHTTPServer.HTTPServer):
+
+    """HTTP server that can be run in a thread and be stopped"""
+
+    do_shutdown = False
+
+    def __init__(self, *args, **kw):
+        for name, value in [("access_logger_name", "httpserver.access"),
+                            ("error_logger_name", "httpserver.error")]:
+            if kw.has_key(name):
+                value = kw[name]
+                del kw[name]
+            setattr(self, name, value)
+        BaseHTTPServer.HTTPServer.__init__(self, *args, **kw)
+
+    def serve_forever(self):
+        """Handle requests until self.do_shutdown is True."""
+        while not self.do_shutdown:
+            self.handle_request()
+
+    def server_close(self):
+        """Shutdown the server.
+
+        Usually this is called from a thread while another thread
+        executes the serve_forever method.  In that case the other
+        thread is most likely blocked while listening for new
+        connections.  After this method returns, the caller will have to
+        do something to unblock the other thread, such as creating a TCP
+        connection to the server.
+        """
+        BaseHTTPServer.HTTPServer.server_close(self)
+        self.do_shutdown = True
+
+
+class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+
+    def log_message(self, format, *args):
+        """Log an arbitrary message.
+
+        Overrides the base class version to write the message into the
+        logger given by the server's access_logger_name.  The messge is
+        logged with INFO level.
+        """
+        logger = logging.getLogger(self.server.access_logger_name)
+        logger.info("%s - - [%s] %s",
+                    self.address_string(),
+                    self.log_date_time_string(),
+                    format % args)
+
+    def log_error(self, *args):
+        """Log an error.
+
+        Overrides the base class method so that the message is written
+        to the logger given by the server's error_logger_name.  The
+        message is logged with the ERROR level.
+        """
+        logger = logging.getLogger(self.server.error_logger_name)
+        logger.error(*args)
+
+
+class ServerThread:
+
+    """Class to run a HTTPServer instance in a thread"""
+
+    def __init__(self, server):
+        """Initializes the server thread with an instance of HTTPServer"""
+        self.server = server
+        self.server_port = self.server.server_port
+        self.server_thread = None
+
+    def start(self, daemon=False):
+        """Starts the server thread"""
+        self.server_thread = threading.Thread(target=self._serve_forever)
+        self.server_thread.setDaemon(daemon)
+        self.server_thread.start()
+
+    def _serve_forever(self):
+        """Helper method to run the server's serve_forever method in a thread"""
+        self.server.serve_forever()
+
+    def stop(self):
+        """Stops the server thread"""
+        if self.server_thread is None:
+            return
+
+        self.server.server_close()
+
+        # The server thread might be blocked while listening on the
+        # socket.  Unblock it by connecting to the port.
+        try:
+            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            s.connect(("localhost", self.server_port))
+            s.close()
+        except socket.error, exc:
+            # The server may have shut down already when we try to
+            # connect, so we ignore connection failures.
+            if exc[0] == errno.ECONNREFUSED:
+                pass
+            else:
+                raise
+
+        self.server_thread.join()
+        self.server_thread = None


Property changes on: trunk/server/osaas/http/httpserver.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/http/run.py
===================================================================
--- trunk/server/osaas/http/run.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/http/run.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,153 @@
+#! /bin/env python
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Functions to run a httpserver as a standalone program"""
+
+import sys
+import os
+import optparse
+import logging
+import signal
+
+import httpserver
+
+
+class SIGTERMException(Exception):
+    pass
+
+
+class OptionStack(object):
+
+    def __init__(self, default_opts, file_opts, commandline_opts):
+        self.__opts = (commandline_opts, file_opts, default_opts)
+
+    def __getattr__(self, attr):
+        for opts in self.__opts:
+            if hasattr(opts, attr):
+                return getattr(opts, attr)
+        raise AttributeError(attr)
+
+    def set_file_option(self, name, value):
+        setattr(self.__opts[1], name, value)
+
+class ProgramWithOptions(object):
+
+    optparse_option_class = None
+
+    def create_option_parser(self, default_port):
+        kw = {}
+        if self.optparse_option_class is not None:
+            kw["option_class"] = self.optparse_option_class
+        parser = optparse.OptionParser(**kw)
+        parser.set_defaults(port=default_port)
+        parser.add_option("--port", type="int")
+        parser.add_option("--access-log")
+        parser.add_option("--error-log")
+        parser.add_option("--config-file")
+        return parser
+
+    def read_config_file(self, opts):
+        """Reads options from a configuratin file
+
+        The parameter opts is an OptionStack instance holding defaults
+        and values read from the command line.  Derived classes wishing
+        to read config files should override this method and set options
+        in opts with its set_file_option method.
+
+        How the configuration file(s) are located and how settings are
+        read is up to derived classes.  However, the base class already
+        provides the config_file option as a standard way to provide the
+        name of a configuration file on the command line.
+        """
+
+    def parse_options(self, args, default_port):
+        parser = self.create_option_parser(default_port=default_port)
+
+        default_opts = parser.get_default_values()
+        configfile_opts = optparse.Values()
+        commandline_opts, rest = parser.parse_args(args, optparse.Values())
+        opts = OptionStack(default_opts, configfile_opts, commandline_opts)
+        self.read_config_file(opts)
+        return opts, rest
+
+
+class ServerProgram(ProgramWithOptions):
+
+    """Main program of the server"""
+
+    def __init__(self):
+        self.server = None
+
+    def setup_signals(self):
+        """Set handle_term_signal as signal handler for SIGTERM"""
+        signal.signal(signal.SIGTERM, self.handle_term_signal)
+
+    def handle_term_signal(self, signum, frame):
+        """Handler for the SIGTERM signal.  Throws SIGTERMException"""
+        raise SIGTERMException
+
+    def setup_logging(self, opts):
+        """Sets up the default loggers of the httpserver based on opts.
+        The function sets up the httpserver.access and httpserver.error
+        loggers.  If opts has attributes access_log or error_log, they
+        are assumed to be the names of the files the corresponding
+        logger should write to.  If one of the attributes does not exist
+        or is None or an empty string, the corresponding logger writes
+        to sys.stderr.
+        """
+        for logtype, format in [("access", "%(message)s"),
+                                ("error",
+                            "%(asctime)s %(levelname)s %(name)s %(message)s")]:
+            logger = logging.getLogger("httpserver." + logtype)
+            logger.setLevel(logging.DEBUG)
+
+            filename = getattr(opts, logtype + "_log")
+            if filename:
+                stream = open(filename, "a")
+            else:
+                stream = sys.stderr
+            hdlr = logging.StreamHandler(stream)
+            hdlr.setFormatter(logging.Formatter(format))
+            logger.addHandler(hdlr)
+
+    def instantiate_server(self, server_class, server_address, opts, **kw):
+        return server_class(server_address, **kw)
+
+    def main(self, server_class=httpserver.HTTPServer,
+             default_port=8989, bind_host="127.0.0.1", **kw):
+        opts, rest = self.parse_options(sys.argv[1:], default_port=default_port)
+
+        self.setup_signals()
+        self.setup_logging(opts)
+
+        logger = logging.getLogger("httpserver.error")
+
+        logger.info("server starting up")
+        server_address = (bind_host, opts.port)
+        self.server = self.instantiate_server(server_class, server_address,
+                                              opts, **kw)
+
+        print "Serving HTTP on port", self.server.server_port, "..."
+        logger.info("serving HTTP on port %d", self.server.server_port)
+
+        try:
+            self.server.serve_forever()
+        except (KeyboardInterrupt, SIGTERMException), err:
+            if isinstance(err, KeyboardInterrupt):
+                logger.info("keyboard interrupt; stopping server")
+            else:
+                logger.info("received SIGTERM; stopping server")
+            self.server.server_close()
+
+        logmessage = "server stopped"
+        print logmessage
+        logger.info(logmessage)
+
+def main(*args, **kw):
+    program = ServerProgram()
+    program.main(*args, **kw)


Property changes on: trunk/server/osaas/http/run.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/http/threadedserver.py
===================================================================
--- trunk/server/osaas/http/threadedserver.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/http/threadedserver.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,45 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+from __future__ import nested_scopes
+
+import logging
+
+from httpserver import HTTPServer
+from threadpool import ThreadPool
+
+
+class ThreadedHTTPServer(HTTPServer):
+
+    def __init__(self, *args, **kw):
+        for name, value in [("num_threads", 5)]:
+            if kw.has_key(name):
+                value = kw[name]
+                del kw[name]
+            setattr(self, name, value)
+        HTTPServer.__init__(self, *args, **kw)
+
+        pool_logger = logging.getLogger(self.error_logger_name + ".workerpool")
+        self.thread_pool = ThreadPool(self.num_threads, lambda f: f(),
+                                      logger=pool_logger)
+        self.thread_pool.start()
+
+    def process_request(self, request, client_address):
+        """Put the request into the queue to be handled by a worker thread
+        """
+        def process_in_worker():
+            try:
+                self.finish_request(request, client_address)
+                self.close_request(request)
+            except:
+                self.handle_error(request, client_address)
+                self.close_request(request)
+        self.thread_pool.put(process_in_worker)
+
+    def server_close(self):
+        HTTPServer.server_close(self)
+        self.thread_pool.stop()


Property changes on: trunk/server/osaas/http/threadedserver.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/http/threadpool.py
===================================================================
--- trunk/server/osaas/http/threadpool.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/http/threadpool.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,85 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Simple pool of worker threads to process tasks like e.g. HTTP requests"""
+
+
+import traceback
+import threading
+import Queue
+
+
+class ThreadPool(object):
+
+    """A class to manage a set of threads that read tasks from a queue.
+
+    The threads managed by the class are started as deamon threads so
+    that it's not required to terminate them if the program using them
+    wants to exit.  Still, they can be explicitly stopped with the stop
+    method.
+
+    The threads read tasks from a Queue object managed by the ThreadPool
+    instance using the queue's blocking get() method.  Tasks taken from
+    the queue are then passed to the handler_function which was
+    specified then the ThreadPool instance was created.  The
+    handler_function is called with one parameter, the task object
+    (which can be any object except None), and should return when the
+    task is finished.  Repeatedly getting new tasks from the queue is
+    handled by the thread pool itself.
+
+    The program using the thread pool puts tasks into the queue with the
+    thread pool's put method.
+    """
+
+    def __init__(self, num_threads, handler_function, logger=None):
+        """Initialize the thread pool with a number of threads and a handler"""
+        self.queue = Queue.Queue(0)
+        self.num_threads = num_threads
+        self.handler_function = handler_function
+        self.logger = logger
+        self.threads = []
+
+    def start(self):
+        """Starts the threads"""
+        if self.logger:
+            self.logger.info("Starting %d threads", self.num_threads)
+        self.threads = []
+        for i in range(self.num_threads):
+            thread = threading.Thread(target = self._thread)
+            thread.setDaemon(1)
+            thread.start()
+            self.threads.append(thread)
+
+    def _thread(self):
+        """Takes tasks repeatedly the queue and calls the handler_function
+        This is an internal method.
+        """
+        while 1:
+            item = self.queue.get()
+            if item is None:
+                break
+            try:
+                self.handler_function(item)
+            except:
+                if self.logger:
+                    self.logger.exception("Exception raised by"
+                                          " threadpool handler function")
+                else:
+                    traceback.print_exc()
+
+    def stop(self):
+        """Stops the worker threads"""
+        if self.logger:
+            self.logger.info("Stopping threads")
+        for i in range(len(self.threads)):
+            self.put(None)
+        for thread in self.threads:
+            thread.join()
+
+    def put(self, item):
+        """Puts item into the queue to be processed by a worker thread"""
+        self.queue.put(item)


Property changes on: trunk/server/osaas/http/threadpool.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/pycompat.py
===================================================================
--- trunk/server/osaas/pycompat.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/pycompat.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,31 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+def sorted(sequence):
+    L = list(sequence)
+    L.sort()
+    return L
+
+# add missing builtins
+import __builtin__
+for name, value in [("sorted", sorted)]:
+    if not hasattr(__builtin__, name):
+        setattr(__builtin__, name, value)
+
+# add some missing functions to existing modules
+
+import base64
+def b64encode(s):
+    return base64.encodestring(s).strip()
+def b64decode(s):
+    return base64.decodestring(s)
+
+for modulename, function in [("base64", b64encode),
+                             ("base64", b64decode)]:
+    module = __import__(modulename)
+    if not hasattr(module, function.__name__):
+        setattr(module, function.__name__, function)


Property changes on: trunk/server/osaas/pycompat.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/recordfilter.py
===================================================================
--- trunk/server/osaas/recordfilter.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/recordfilter.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,11 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Filter for requests"""
+
+def default_filter(record):
+    return record.SERVICE == "WMS" and record.REQUEST == "GetMap"


Property changes on: trunk/server/osaas/recordfilter.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/run.py
===================================================================
--- trunk/server/osaas/run.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/run.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,36 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+from osaas.http.run import ServerProgram
+from osaas.server import OSAASServer
+from osaas.config import OSAASOption, read_config_file
+
+class OSAASServerProgram(ServerProgram):
+
+    optparse_option_class = OSAASOption
+
+    def create_option_parser(self, **kw):
+        self.parser = ServerProgram.create_option_parser(self, **kw)
+        for name in ["port", "access-log", "error-log"]:
+            path = "OSAASConfig/" + "".join([part.capitalize()
+                                             for part in name.split("-")])
+            self.parser.get_option("--" + name).xml_path = path
+        self.parser.add_option("--userdb-file", xml_path="OSAASConfig/UserDB")
+        return self.parser
+
+    def instantiate_server(self, server_class, server_address, opts, **kw):
+        return server_class(server_address, userdbfile=opts.userdb_file, **kw)
+
+    def read_config_file(self, opts):
+        if opts.config_file is not None:
+            read_config_file(opts.config_file, self.parser.option_list,
+                             opts.set_file_option)
+
+
+def main():
+    program = OSAASServerProgram()
+    program.main(server_class=OSAASServer)


Property changes on: trunk/server/osaas/run.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/server.py
===================================================================
--- trunk/server/osaas/server.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/server.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,132 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+import logging
+import cgi
+import base64
+
+from BaseHTTPServer import BaseHTTPRequestHandler
+
+from osaas.http.httpserver import HTTPRequestHandler
+from osaas.http.threadedserver import ThreadedHTTPServer
+from osaas.http.threadpool import ThreadPool
+from dbbackend import dbhandler
+from recordfilter import default_filter
+from userdb import UserDB
+from formparser import parse_formdata, FormParserException
+
+
+def parse_basic_authentication(authorization):
+    """Parse a HTTP Basic authentication header and return (username, password).
+    If the header cannot be parsed a ValueError exception is raised.
+    """
+    if authorization.startswith("Basic "):
+        credentials_b64 = authorization.split()[1]
+        credentials = base64.b64decode(credentials_b64)
+        fields = credentials.split(":", 1)
+        if len(fields) == 2:
+            return fields
+        else:
+            raise ValueError("Malformed Basic auth informaion")
+    else:
+        raise ValueError("Unsupported authorization scheme")
+
+
+class OSAASRequestHandler(HTTPRequestHandler):
+
+    responses = HTTPRequestHandler.responses.copy()
+    responses.update({
+        411: ('Length Required', 'Client must specify Content-Length.'),
+        415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
+        })
+
+    def do_POST(self):
+        try:
+            self.handle_owsaccounting_formdata()
+        except:
+            self.log_exception("An unexpected exception occurred;"
+                               " sending 500 response")
+            self.send_error(500)
+
+    def handle_owsaccounting_formdata(self):
+        if self.path != "/owsaccounting/":
+            self.send_error(404)
+            return
+
+        # check authentication
+        if not self.check_user():
+            return
+
+        # check for the expected content type
+        content_type = self.headers.getheader("Content-Type")
+        if content_type != "application/x-www-form-urlencoded":
+            self.send_error(415)
+            return
+
+        # If any data is associated with the request read it
+        length = int(self.headers.getheader("Content-Length", "0"))
+        if length:
+            # FIXME: check whether length bytes were read
+            data = self.rfile.read(length)
+        else:
+            self.send_error(411)
+            return
+
+        try:
+            record = parse_formdata(data)
+        except FormParserException, err:
+            self.send_error(400, "Bad request: %s" % err)
+            return
+
+        if default_filter(record):
+            self.log_debug("passing record to db thread")
+            self.server.dbthreadpool.put(record)
+        else:
+            self.log_debug("record (SERVICE=%r, REQUEST=%r) was filtered out",
+                           record.SERVICE, record.REQUEST)
+
+        self.send_response(200)
+
+    def check_user(self):
+        authorization = self.headers.getheader("Authorization")
+        try:
+            if authorization:
+                username, password = parse_basic_authentication(authorization)
+                if self.server.userdb.check_credentials(username, password):
+                    return True
+        except:
+            self.log_exception("Error parsing Authorization header")
+        self.send_response(401)
+        self.send_header("WWW-Authenticate", 'Basic realm="osaas"')
+        return False
+
+    def log_debug(self, *args, **kw):
+        logger = logging.getLogger(self.server.error_logger_name)
+        logger.debug(*args, **kw)
+
+    def log_exception(self, *args, **kw):
+        logger = logging.getLogger(self.server.error_logger_name)
+        logger.debug(exc_info=1, *args, **kw)
+
+
+class OSAASServer(ThreadedHTTPServer):
+
+    def __init__(self, serveraddress, RequestHandlerClass=OSAASRequestHandler,
+                 dbhandler=dbhandler, userdbfile=None,
+                 **kw):
+        ThreadedHTTPServer.__init__(self, serveraddress, RequestHandlerClass,
+                                    **kw)
+        self.userdb = UserDB()
+        if userdbfile:
+            self.userdb.read(userdbfile)
+        logger = logging.getLogger(self.error_logger_name + ".dbpool")
+        self.dbthreadpool = ThreadPool(1, dbhandler, logger=logger)
+        self.dbthreadpool.start()
+
+    def server_close(self):
+        ThreadedHTTPServer.server_close(self)
+        self.dbthreadpool.stop()


Property changes on: trunk/server/osaas/server.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/osaas/userdb.py
===================================================================
--- trunk/server/osaas/userdb.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/osaas/userdb.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,76 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Simple User database"""
+
+import sha
+import base64
+import errno
+
+SSHA_PREFIX = "{SSHA}"
+
+def random_salt(length):
+    random = open("/dev/urandom")
+    try:
+        return random.read(length)
+    finally:
+        random.close()
+
+def encode_ssha(password, salt):
+    digester = sha.new(password)
+    digester.update(salt)
+    return SSHA_PREFIX + base64.b64encode(digester.digest() + salt)
+
+def check_password(password, hashed):
+    if hashed.startswith(SSHA_PREFIX):
+        salt = base64.b64decode(hashed[len(SSHA_PREFIX):])[20:]
+        return encode_ssha(password, salt) == hashed
+    else:
+        return False
+
+class UserDB(object):
+
+    def __init__(self, users=()):
+        self.users = {}
+        for username, password in users:
+            self.add_user(username, password)
+
+    def read(self, filename):
+        try:
+            pwfile = open(filename)
+        except IOError, err:
+            # If the file doesn't exist, treat it as an empty file
+            if err.errno == errno.ENOENT:
+                return
+        try:
+            while 1:
+                line = pwfile.readline()
+                if not line:
+                    break
+
+                fields = line.strip().split(":", 1)
+                if len(fields) == 2:
+                    self.users[fields[0]] = fields[1]
+                else:
+                    # FIXME: warn about this
+                    pass
+        finally:
+            pwfile.close()
+
+    def write(self, filename):
+        pwfile = open(filename, "w")
+        for username, pwhash in sorted(self.users.items()):
+            pwfile.write("%s:%s\n" % (username, pwhash))
+        pwfile.close()
+
+    def add_user(self, username, password):
+        self.users[username] = encode_ssha(password, random_salt(8))
+
+    def check_credentials(self, username, password):
+        if self.users.has_key(username):
+            return check_password(password, self.users[username])
+        return False


Property changes on: trunk/server/osaas/userdb.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/startosaas.py
===================================================================
--- trunk/server/startosaas.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/startosaas.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,11 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+from osaas.run import main
+
+if __name__ == "__main__":
+    main()


Property changes on: trunk/server/startosaas.py
___________________________________________________________________
Name: svn:executable
   + *
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/test/runtests.py
===================================================================
--- trunk/server/test/runtests.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/runtests.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,76 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""
+Main entry point for the test suite.
+
+Just run this file as a python script to execute all tests
+"""
+
+import os
+import sys
+import unittest
+import getopt
+
+try:
+    __file__
+except NameError:
+    __file__ = sys.argv[0]
+test_dir = os.path.dirname(__file__)
+sys.path.append(os.path.join(test_dir, os.pardir))
+
+def find_test_modules(dirname, package = None):
+    """Return a list the names of the test modules in the directory dirname
+
+    The return value is a list of names that can be passed to
+    unittest.defaultTestLoader.loadTestsFromNames.  Each name of the
+    list is the name of a pure python module, one for each file in
+    dirname whose name starts with 'test' and ends with '.py'.
+
+    The optional parameter package should be the name of the python
+    package whose directory is dirname.  If package is given all names
+    in the returned list will be prefixed with package and a dot.
+    """
+    if package:
+        prefix = package + "."
+    else:
+        prefix = ""
+
+    suffix = ".py"
+    return [prefix + name[:-len(suffix)]
+            for name in os.listdir(dirname)
+                if name.startswith("test") and name.endswith(suffix)]
+
+
+def main():
+    """Run all the tests in the test suite"""
+    verbosity = 1
+
+    opts, rest = getopt.getopt(sys.argv[1:], "v")
+    for optchar, optvalue in opts:
+        if optchar == "-v":
+            verbosity = 2
+        else:
+            raise RuntimeError("Unknown option %r" % optchar)
+
+    # Build the list of test names.  If names were given on the command
+    # line, run exactly those.  Othwerwise build a default list of
+    # names.
+    if rest:
+        names = rest
+    else:
+        # All Python files starting with 'test' in the current directory
+        # and some directories in Extensions contain test cases.
+        names = find_test_modules(test_dir)
+    suite = unittest.defaultTestLoader.loadTestsFromNames(names)
+    runner = unittest.TextTestRunner(verbosity=verbosity)
+    result = runner.run(suite)
+    sys.exit(not result.wasSuccessful())
+
+
+if __name__ == "__main__":
+    main()


Property changes on: trunk/server/test/runtests.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/test/serversupport.py
===================================================================
--- trunk/server/test/serversupport.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/serversupport.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,137 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Support code for tests involving osaas.http.httpserver"""
+
+import logging
+import unittest
+import httplib
+import re
+import time
+from math import floor
+import StringIO
+
+
+import osaas.http.httpserver as httpserver
+
+
+class HTTPRequestHandler(httpserver.HTTPRequestHandler):
+
+    def do_GET(self):
+        """Handle a GET request"""
+        for path, headers, data in self.server.contents:
+            if path == self.path:
+                self.send_response(200)
+                for header, value in headers:
+                    self.send_header(header, value)
+                self.send_header("Content-Length", len(data))
+                self.end_headers()
+                self.wfile.write(data)
+                return
+        else:
+            self.send_error(404)
+
+
+class HTTPServer(httpserver.HTTPServer):
+
+    def __init__(self, contents, server_address=("127.0.0.1", 0),
+                 RequestHandlerClass=HTTPRequestHandler, **kw):
+        self.contents = contents
+        httpserver.HTTPServer.__init__(self, server_address,
+                                       RequestHandlerClass, **kw)
+
+
+class LoggerMixin:
+
+    def new_logger(self, logger_name, format="%(message)s"):
+        """Create a new logger with some default configuration.
+
+        The return value is the StringIO object used as the stream into
+        which the logger writes.
+        """
+        logger = logging.getLogger(logger_name)
+        logger.setLevel(logging.DEBUG)
+        logger.propagate = False
+        stream = StringIO.StringIO()
+        hdlr = logging.StreamHandler(stream)
+        hdlr.setFormatter(logging.Formatter(format))
+        logger.addHandler(hdlr)
+        return stream
+
+
+class ServerTest(unittest.TestCase, LoggerMixin):
+
+    access_logger_name = "test.access"
+    error_logger_name = "test.error"
+
+    def setUp(self):
+        self.access_log_stream = self.new_logger(self.access_logger_name)
+        self.error_log_stream = self.new_logger(self.error_logger_name)
+
+        server = self.create_server(("127.0.0.1", 0),
+                                    access_logger_name=self.access_logger_name,
+                                    error_logger_name=self.error_logger_name)
+        server.test_case = self
+        self.server = httpserver.ServerThread(server)
+        self.server.start(daemon=True)
+
+        self.test_start_time = time.time()
+
+    def create_server(self, *args, **kw):
+        return httpserver.HTTPServer(*args, **kw)
+
+    def tearDown(self):
+        self.server.stop()
+
+    def reset_access_log(self):
+        self.access_log_stream.truncate(0)
+
+    def check_log(self, expected_lines):
+        log_lines = self.access_log_stream.getvalue().splitlines()
+
+        for idx in range(len(log_lines)):
+            line = log_lines[idx]
+
+            # extract the timestamp from the line so that we can check
+            # the contents of the line without the ever changing time
+            # stamp and check the timestamp value separately
+            match = re.search(r"\[([^]]+)\]", line)
+            if match:
+                line = line[:match.start(1)] + line[match.end(1):]
+
+                # Check time stamps.  Round both time stamps down to the
+                # nearest second. Otherwise start_time might actually be
+                # larger than timestamp because timestamp only has 1s
+                # accuracy
+                # FIXME: enforce "C" locale for strptime
+                timestamp = time.mktime(time.strptime(match.group(1),
+                                                      "%d/%b/%Y %H:%M:%S"))
+                self.failUnless(floor(timestamp) >= floor(self.test_start_time))
+
+            log_lines[idx] = line
+
+        self.assertEquals(log_lines, expected_lines)
+
+
+    def run_simple_http_test(self, path, status, access_log=None,
+                             method="GET", extra_headers=(), body=None):
+        self.reset_access_log()
+        http = httplib.HTTPConnection("localhost", self.server.server_port)
+        http.putrequest(method, path)
+        for header, value in extra_headers:
+            http.putheader(header, value)
+        http.endheaders()
+        if body is not None:
+            http.send(body)
+        response = http.getresponse()
+        self.assertEquals(response.status, status)
+
+        if access_log is None:
+            access_log = ['localhost - - [] "%s %s HTTP/1.1" %d -'
+                          % (method, path, status)]
+        self.check_log(access_log)
+        return response.read()


Property changes on: trunk/server/test/serversupport.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/test/support.py
===================================================================
--- trunk/server/test/support.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/support.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,60 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Support code for the test cases"""
+
+import os
+
+def create_temp_dir():
+    """Create a temporary directory for the test-suite and return its name.
+
+    The temporary directory is always called temp and is created in the
+    directory where the support module is located.
+
+    If the temp directory already exists, just return the name.
+    """
+    name = os.path.abspath(os.path.join(os.path.dirname(__file__), "temp"))
+
+    # if the directory already exists, we're done
+    if os.path.isdir(name):
+        return name
+
+    # create the directory
+    os.mkdir(name)
+    return name
+
+
+class FileTestMixin:
+
+    """Mixin class for tests that use files in the temporary directory
+    """
+
+    def temp_file_name(self, basename, remove=False):
+        """Return the full name of the file named basename in the temp. dir.
+        If the remove parameter is true, the file is removed if already exists.
+        """
+        filename = os.path.join(create_temp_dir(), basename)
+        if remove and os.path.exists(filename):
+            os.remove(filename)
+        return filename
+
+    def create_temp_file(self, basename, contents, mode = None):
+        """Create a file in the temp directory"""
+        filename = self.temp_file_name(basename)
+        file = open(filename, "w")
+        file.write(contents)
+        file.close()
+        if mode is not None:
+            os.chmod(filename, mode)
+        return filename
+
+
+class AttributeTestMixin:
+
+    def check_attributes(self, obj, **attrs):
+        for key, value in attrs.items():
+            self.assertEquals(getattr(obj, key), value)


Property changes on: trunk/server/test/support.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/test/test_config.py
===================================================================
--- trunk/server/test/test_config.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_config.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,83 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+import unittest
+
+from support import FileTestMixin
+from osaas.config import OSAASOption, ConfigXMLPathError
+from osaas.run import OSAASServerProgram
+
+class TestOSAASOption(unittest.TestCase):
+
+    def test_xml_path(self):
+        opt = OSAASOption("--port", type="int", xml_path="OSAASConfig/Port")
+        self.assertEquals(opt.xml_path, "OSAASConfig/Port")
+
+
+
+class ConfigParserTest(unittest.TestCase, FileTestMixin):
+
+    config = ""
+
+    def setUp(self):
+        self.filename = self.create_temp_file(self.id() + ".cfg", self.config)
+
+
+class TestConfigParsing(ConfigParserTest):
+
+    config = """\
+<?xml version='1.0'?>
+<OSAASConfig>
+  <Port>6688</Port>
+  <AccessLog>/var/log/osaas-access.log</AccessLog>
+  <ErrorLog>/var/log/osaas-error.log</ErrorLog>
+</OSAASConfig>
+"""
+
+    def test(self):
+        prog = OSAASServerProgram()
+        opts, rest = prog.parse_options(["--config-file=%s" % self.filename],
+                                        default_port=9090)
+        self.assertEquals(opts.port, 6688)
+        self.assertEquals(opts.access_log, "/var/log/osaas-access.log")
+        self.assertEquals(opts.error_log, "/var/log/osaas-error.log")
+
+
+class TestConfigParsing_MissingEntries(ConfigParserTest):
+
+    config = """\
+<?xml version='1.0'?>
+<OSAASConfig>
+  <ErrorLog>/var/log/osaas-error.log</ErrorLog>
+</OSAASConfig>
+"""
+
+    def test(self):
+        prog = OSAASServerProgram()
+        opts, rest = prog.parse_options(["--config-file=%s" % self.filename],
+                                        default_port=9090)
+        self.assertEquals(opts.port, 9090)
+        self.assertEquals(opts.access_log, None)
+        self.assertEquals(opts.error_log, "/var/log/osaas-error.log")
+
+
+class TestConfigParsing_AmbiguousEntries(ConfigParserTest):
+
+    config = """\
+<?xml version='1.0'?>
+<OSAASConfig>
+  <ErrorLog>/var/log/osaas-error.log</ErrorLog>
+  <ErrorLog>/var/log/osaas-error2.log</ErrorLog>
+</OSAASConfig>
+"""
+
+    def test(self):
+        prog = OSAASServerProgram()
+        self.assertRaises(ConfigXMLPathError,
+                          prog.parse_options,
+                          ["--config-file=%s" % self.filename],
+                          default_port=9090)


Property changes on: trunk/server/test/test_config.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/test/test_formparser.py
===================================================================
--- trunk/server/test/test_formparser.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_formparser.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,142 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for osaas.formparser"""
+
+import sys
+import unittest
+import traceback
+
+import support
+
+from osaas.formparser import parse_formdata, FormParserException
+
+
+class FormParsingTests(unittest.TestCase, support.AttributeTestMixin):
+
+    def test_convert_form_urlencoded(self):
+        formdata = "&".join(["user=karl", "responsetime=2007-06-01T12:14:16Z",
+                             "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+                             "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+                             "requeststring=VERSION=1.1.1%26REQUEST=GetMap"
+                             "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+                             "%26WIDTH=460%26HEIGHT=348"
+                             "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+                             "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+                             "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+                             "%26SERVICE=WMS"])
+        self.check_attributes(parse_formdata(formdata),
+                              user="karl",
+                              responsetime="2007-06-01T12:14:16Z",
+                              wmsidextern="example.com/cgi-bin/fridawms",
+                              wmsidintern="localhost/cgi-bin/myfrida",
+                              requeststring=("VERSION=1.1.1&REQUEST=GetMap"
+                                             "&FORMAT=image/png"
+                                             "&TRANSPARENT=TRUE"
+                                             "&WIDTH=460&HEIGHT=348"
+                                      "&EXCEPTIONS=application/vnd.ogc.se_xml"
+                                             "&BGCOLOR=0xffffff"
+                                             "&BBOX=0.0,0.0,460.0,348.0"
+                                             "&LAYERS=gewaesser&STYLES="
+                                             "&SRS=EPSG:4326"
+                                             "&SERVICE=WMS"),
+                              VERSION="1.1.1",
+                              REQUEST="GetMap",
+                              FORMAT="image/png",
+                              TRANSPARENT="TRUE",
+                              WIDTH="460",
+                              HEIGHT="348",
+                              EXCEPTIONS="application/vnd.ogc.se_xml",
+                              BGCOLOR="0xffffff",
+                              BBOX="0.0,0.0,460.0,348.0",
+                              LAYERS="gewaesser",
+                              STYLES="",
+                              SRS="EPSG:4326",
+                              SERVICE="WMS")
+
+    def check_exception(self, exception_class, exception_message,
+                        function, *args, **kw):
+        try:
+            function(*args, **kw)
+        except exception_class, err:
+            self.assertEquals(str(err), exception_message)
+        except:
+            self.fail("Unexpected exception:\n%s"
+                      % "".join(traceback.format_exception(*sys.exc_info())))
+
+
+    def test_missing_required_parameters_in_formdata(self):
+        # formdata is missing the required field "user"
+        formdata = "&".join(["responsetime=2007-06-01T12:14:16Z",
+                             "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+                             "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+                             "requeststring=VERSION=1.1.1%26REQUEST=GetMap"
+                             "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+                             "%26WIDTH=460%26HEIGHT=348"
+                             "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+                             "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+                             "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+                             "%26SERVICE=WMS"])
+        self.check_exception(FormParserException,
+                             "formdata is missing required field 'user'",
+                             parse_formdata, formdata)
+
+    def test_missing_required_parameters_in_requeststring(self):
+        # requeststring is missing the required field "REQUEST"
+        formdata = "&".join(["user=karl", "responsetime=2007-06-01T12:14:16Z",
+                             "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+                             "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+                             "requeststring=VERSION=1.1.1"
+                             "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+                             "%26WIDTH=460%26HEIGHT=348"
+                             "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+                             "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+                             "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+                             "%26SERVICE=WMS"])
+        self.check_exception(FormParserException,
+                             "formdata is missing required field 'REQUEST'",
+                             parse_formdata, formdata)
+
+    def test_duplicate_parameters(self):
+        # The responsetime occurs twice
+        formdata = "&".join(["user=karl", "responsetime=2007-06-01T12:14:16Z",
+                             "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+                             "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+                             "requeststring=VERSION=1.1.1"
+                             "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+                             "%26WIDTH=460%26HEIGHT=348%26REQUEST=GetMap"
+                             "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+                             "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+                             "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+                             "%26SERVICE=WMS",
+                             "responsetime=2007-06-01T12:14:16Z"])
+        self.check_exception(FormParserException,
+                             "only one value allowed for field 'responsetime'",
+                             parse_formdata, formdata)
+
+    def test_duplicate_parameters_in_requeststring(self):
+        # The SERVICE field occurs twice in the requeststring
+        formdata = "&".join(["user=karl", "responsetime=2007-06-01T12:14:16Z",
+                             "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+                             "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+                             "requeststring=VERSION=1.1.1%26REQUEST=GetMap"
+                             "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+                             "%26WIDTH=460%26HEIGHT=348%26SERVICE=WMS"
+                             "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+                             "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+                             "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+                             "%26SERVICE=WMS"])
+        self.check_exception(FormParserException,
+                             "only one value allowed for field 'SERVICE'",
+                             parse_formdata, formdata)
+
+    def test_invalid_formdata_syntax_no_equals(self):
+        # No equals sign in form field
+        formdata = "user"
+        self.check_exception(FormParserException,
+                             "Error parsing form data: bad query field: 'user'",
+                             parse_formdata, formdata)


Property changes on: trunk/server/test/test_formparser.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/test/test_httpserver.py
===================================================================
--- trunk/server/test/test_httpserver.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_httpserver.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,35 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for osaas.http.httpserver"""
+
+import serversupport
+
+import osaas.http.httpserver as httpserver
+
+
+class SimpleHTTPServerTests(serversupport.ServerTest):
+
+    server_contents = [
+        ("/wms", [("Content-Type", "text/plain")], "wms data"),
+        ]
+
+    def create_server(self, *args, **kw):
+        return serversupport.HTTPServer(self.server_contents, *args, **kw)
+
+    def test_get_200(self):
+        self.run_simple_http_test("/wms", 200)
+        self.assertEquals(self.error_log_stream.getvalue(), "")
+
+    def test_get_404(self):
+        self.run_simple_http_test("/something/that/doesnt/exist", 404)
+        # The case of the default messages in BaseHTTPServer has changed
+        # between Python 2.1 and 2.4.  Compare lower case strings to
+        # avoid test failures because of that.
+        self.assertEquals(self.error_log_stream.getvalue().lower(),
+                          'code 404, message not found\n')
+


Property changes on: trunk/server/test/test_httpserver.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/test/test_osasserver.py
===================================================================
--- trunk/server/test/test_osasserver.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_osasserver.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,242 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for osaas.server"""
+
+import base64
+import threading
+
+import serversupport
+import support
+
+from osaas.server import OSAASServer
+
+
+class OSAASServerTests(serversupport.ServerTest, support.FileTestMixin):
+
+    def setUp(self):
+        self.userdbfile = self.create_temp_file(self.id() + "-userdb",
+                        "karl:{SSHA}NGVfMfEuUjT3wFNgP0KiqWs47m9zYWx6")
+        self.authentication_headers = [
+            ("Authorization", "Basic %s" % base64.b64encode("karl:xyzzy")),
+            ]
+        serversupport.ServerTest.setUp(self)
+
+    def create_server(self, *args, **kw):
+        return OSAASServer(userdbfile=self.userdbfile, *args, **kw)
+
+    def test_post_wrong_path(self):
+        self.run_simple_http_test("/some/path", 404, method="POST",
+                                  extra_headers=self.authentication_headers)
+
+    def test_post_no_content_type(self):
+        self.run_simple_http_test("/owsaccounting/", 415, method="POST",
+                                  extra_headers=self.authentication_headers)
+
+    def test_post_no_content_length(self):
+        extra_headers = [("Content-Type", "application/x-www-form-urlencoded")]
+        self.run_simple_http_test("/owsaccounting/", 411, method="POST",
+                                  extra_headers=(extra_headers
+                                                 + self.authentication_headers))
+
+    def test_unauthenticated(self):
+        body = "requeststring=SERVICE=WMS%26REQUEST=GetMap"
+        extra_headers = [("Content-Type", "application/x-www-form-urlencoded"),
+                         ("Content-Length", len(body))]
+        self.run_simple_http_test("/owsaccounting/", 401, method="POST",
+                                  extra_headers=extra_headers,
+                                  body=body)
+
+
+    def test_malformed_basic_authentication(self):
+        # If the authorization were successful we would get 415 errors
+        # because of the missing content type.  With malformed
+        # authentication headers we should get 401.
+
+        # sanity check first: do we get 415 with correct authentication?
+        self.run_simple_http_test("/owsaccounting/", 415, method="POST",
+                                  extra_headers=self.authentication_headers)
+
+        malformed = [("Authorization",
+                      "Basic %s" % base64.b64encode("hans 123"))]
+        self.run_simple_http_test("/owsaccounting/", 401, method="POST",
+                                  extra_headers=malformed)
+
+    def test_malformed_basic_authentication2(self):
+        # If the authorization were successful we would get 415 errors
+        # because of the missing content type.  With malformed
+        # authentication headers we should get 401.
+
+        # sanity check first: do we get 415 with correct authentication?
+        self.run_simple_http_test("/owsaccounting/", 415, method="POST",
+                                  extra_headers=self.authentication_headers)
+
+        malformed = [("Authorization", "Basic ")]
+        self.run_simple_http_test("/owsaccounting/", 401, method="POST",
+                                  extra_headers=malformed)
+
+    def test_authentication_header_but_not_basic(self):
+        # If the authorization were successful we would get 415 errors
+        # because of the missing content type.  With malformed
+        # authentication headers we should get 401.
+
+        # sanity check first: do we get 415 with correct authentication?
+        self.run_simple_http_test("/owsaccounting/", 415, method="POST",
+                                  extra_headers=self.authentication_headers)
+
+        malformed = [("Authorization", "Invented data")]
+        self.run_simple_http_test("/owsaccounting/", 401, method="POST",
+                                  extra_headers=malformed)
+
+
+class OSAASDBServerTests(serversupport.ServerTest, support.AttributeTestMixin,
+                         support.FileTestMixin):
+
+    def setUp(self):
+        self.dbrequests = []
+        self.dbrequest_event = threading.Event()
+        self.userdbfile = self.create_temp_file(self.id() + "-userdb",
+                        "karl:{SSHA}NGVfMfEuUjT3wFNgP0KiqWs47m9zYWx6")
+        self.authentication_headers = [
+            ("Authorization", "Basic %s" % base64.b64encode("karl:xyzzy")),
+            ]
+        serversupport.ServerTest.setUp(self)
+
+    def dbhandler(self, req):
+        self.dbrequests.append(req)
+        self.dbrequest_event.set()
+
+    def create_server(self, *args, **kw):
+        return OSAASServer(dbhandler=self.dbhandler, userdbfile=self.userdbfile,
+                           *args, **kw)
+
+    def send_formdata_request(self, formdata, expected_status=200):
+        extra_headers = ([("Content-Type", "application/x-www-form-urlencoded"),
+                         ("Content-Length", len(formdata))]
+                         + self.authentication_headers)
+        return self.run_simple_http_test("/owsaccounting/", expected_status,
+                                         method="POST",
+                                         extra_headers=extra_headers,
+                                         body=formdata)
+
+    def test_post_with_content(self):
+        formdata = "&".join([
+            "user=karl", "responsetime=2007-06-01T12:14:16Z",
+            "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+            "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+            "requeststring=VERSION=1.1.1%26REQUEST=GetMap"
+            "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+            "%26WIDTH=460%26HEIGHT=348"
+            "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+            "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+            "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+            "%26SERVICE=WMS"])
+        self.send_formdata_request(formdata)
+        self.dbrequest_event.wait(1)
+        self.assertEquals(len(self.dbrequests), 1)
+        self.check_attributes(self.dbrequests[0],
+                              user="karl",
+                              responsetime="2007-06-01T12:14:16Z",
+                              wmsidextern="example.com/cgi-bin/fridawms",
+                              wmsidintern="localhost/cgi-bin/myfrida",
+                              requeststring=("VERSION=1.1.1&REQUEST=GetMap"
+                                             "&FORMAT=image/png"
+                                             "&TRANSPARENT=TRUE"
+                                             "&WIDTH=460&HEIGHT=348"
+                                      "&EXCEPTIONS=application/vnd.ogc.se_xml"
+                                             "&BGCOLOR=0xffffff"
+                                             "&BBOX=0.0,0.0,460.0,348.0"
+                                             "&LAYERS=gewaesser&STYLES="
+                                             "&SRS=EPSG:4326"
+                                             "&SERVICE=WMS"),
+                              VERSION="1.1.1",
+                              REQUEST="GetMap",
+                              FORMAT="image/png",
+                              TRANSPARENT="TRUE",
+                              WIDTH="460",
+                              HEIGHT="348",
+                              EXCEPTIONS="application/vnd.ogc.se_xml",
+                              BGCOLOR="0xffffff",
+                              BBOX="0.0,0.0,460.0,348.0",
+                              LAYERS="gewaesser",
+                              STYLES="",
+                              SRS="EPSG:4326",
+                              SERVICE="WMS")
+
+
+    def test_post_with_content_and_filtering(self):
+        self.send_formdata_request("&".join([
+            "user=karl", "responsetime=2007-06-01T12:14:16Z",
+            "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+            "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+            "requeststring=VERSION=1.1.1%26REQUEST=GetMap"
+            "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+            "%26WIDTH=460%26HEIGHT=348"
+            "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+            "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+            "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+            "%26SERVICE=WMS"]))
+        self.send_formdata_request("&".join([
+            "user=karl", "responsetime=2007-06-01T12:14:17Z",
+            "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+            "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+            "requeststring=VERSION=1.1.1%26REQUEST=GetCapabilities"
+            "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+            "%26WIDTH=460%26HEIGHT=348"
+            "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+            "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+            "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+            "%26SERVICE=WMS"]))
+
+        self.dbrequest_event.wait(1)
+        self.assertEquals(len(self.dbrequests), 1)
+        self.check_attributes(self.dbrequests[0],
+                              user="karl",
+                              responsetime="2007-06-01T12:14:16Z",
+                              wmsidextern="example.com/cgi-bin/fridawms",
+                              wmsidintern="localhost/cgi-bin/myfrida",
+                              requeststring=("VERSION=1.1.1&REQUEST=GetMap"
+                                             "&FORMAT=image/png"
+                                             "&TRANSPARENT=TRUE"
+                                             "&WIDTH=460&HEIGHT=348"
+                                      "&EXCEPTIONS=application/vnd.ogc.se_xml"
+                                             "&BGCOLOR=0xffffff"
+                                             "&BBOX=0.0,0.0,460.0,348.0"
+                                             "&LAYERS=gewaesser&STYLES="
+                                             "&SRS=EPSG:4326"
+                                             "&SERVICE=WMS"),
+                              VERSION="1.1.1",
+                              REQUEST="GetMap",
+                              FORMAT="image/png",
+                              TRANSPARENT="TRUE",
+                              WIDTH="460",
+                              HEIGHT="348",
+                              EXCEPTIONS="application/vnd.ogc.se_xml",
+                              BGCOLOR="0xffffff",
+                              BBOX="0.0,0.0,460.0,348.0",
+                              LAYERS="gewaesser",
+                              STYLES="",
+                              SRS="EPSG:4326",
+                              SERVICE="WMS")
+
+    def test_post_with_content_with_missing_fields(self):
+        # the formdata is missing the "responsetime" field
+        formdata = "&".join([
+            "user=karl",
+            "wmsidextern=example.com%2Fcgi-bin%2Ffridawms",
+            "wmsidintern=localhost%2Fcgi-bin%2Fmyfrida",
+            "requeststring=VERSION=1.1.1%26REQUEST=GetMap"
+            "%26FORMAT=image%2Fpng%26TRANSPARENT=TRUE"
+            "%26WIDTH=460%26HEIGHT=348"
+            "%26EXCEPTIONS=application%2Fvnd.ogc.se_xml"
+            "%26BGCOLOR=0xffffff%26BBOX=0.0,0.0,460.0,348.0"
+            "%26LAYERS=gewaesser%26STYLES=%26SRS=EPSG:4326"
+            "%26SERVICE=WMS"])
+        response_text = self.send_formdata_request(formdata,
+                                                   expected_status=400)
+        self.failUnless("formdata is missing required field 'responsetime'"
+                        in response_text)


Property changes on: trunk/server/test/test_osasserver.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/test/test_run.py
===================================================================
--- trunk/server/test/test_run.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_run.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,122 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for osaas.http.run"""
+
+import sys
+import unittest
+import optparse
+import traceback
+
+from osaas.http.run import ProgramWithOptions, OptionStack
+
+
+class TestProgramWithOptions(unittest.TestCase):
+
+    def test_default_options(self):
+        prog = ProgramWithOptions()
+        opts, rest = prog.parse_options([], default_port=12345)
+        self.assertEquals(opts.port, 12345)
+        self.assertEquals(opts.error_log, None)
+        self.assertEquals(opts.access_log, None)
+
+    def test_options(self):
+        prog = ProgramWithOptions()
+        opts, rest = prog.parse_options(["--port=4321",
+                                         "--error-log=/var/log/testerror.log"],
+                                        default_port=12345)
+        self.assertEquals(opts.port, 4321)
+        self.assertEquals(opts.error_log, "/var/log/testerror.log")
+        self.assertEquals(opts.access_log, None)
+
+    def test_adding_options(self):
+        class Program(ProgramWithOptions):
+            def create_option_parser(self, **kw):
+                parser = ProgramWithOptions.create_option_parser(self, **kw)
+                parser.add_option("--num-threads", type="int")
+                parser.set_defaults(num_threads=5)
+                return parser
+
+        # defaults
+        prog = Program()
+        opts, rest = prog.parse_options([], default_port=12345)
+        self.assertEquals(opts.port, 12345)
+        self.assertEquals(opts.error_log, None)
+        self.assertEquals(opts.access_log, None)
+        self.assertEquals(opts.num_threads, 5)
+
+        # some options supplied
+        prog = Program()
+        opts, rest = prog.parse_options(["--port=6543", "--num-threads=10"],
+                                        default_port=12345)
+        self.assertEquals(opts.port, 6543)
+        self.assertEquals(opts.error_log, None)
+        self.assertEquals(opts.access_log, None)
+        self.assertEquals(opts.num_threads, 10)
+
+    def test_read_config_file_method(self):
+        class Program(ProgramWithOptions):
+            def read_config_file(self, opts):
+                opts.set_file_option("port", 9988)
+                opts.set_file_option("access_log", "/home/test/access.log")
+
+        prog = Program()
+        opts, rest = prog.parse_options(["--port=8877"], default_port=12345)
+
+        # The port has been given on the command line, so that value
+        # should win:
+        self.assertEquals(opts.port, 8877)
+
+        # The access_log has been set in read_config_file but not on the
+        # command line, so the value from read_config_file wins:
+        self.assertEquals(opts.access_log, "/home/test/access.log")
+
+        # The error_log has only been set in the defaults (with the
+        # default default value of None).
+        self.assertEquals(opts.error_log, None)
+
+    def test_optparse_option_class(self):
+        class MyOption(optparse.Option):
+            ATTRS = optparse.Option.ATTRS[:]
+            ATTRS.extend(["extra_arg"])
+
+        class Program(ProgramWithOptions):
+            optparse_option_class = MyOption
+            def create_option_parser(self, **kw):
+                parser = ProgramWithOptions.create_option_parser(self, **kw)
+                # the default option class will raise an error when an
+                # unknown argument like "extra_arg" is passed.
+                parser.add_option("--some-option", extra_arg=True)
+                return parser
+
+        prog = Program()
+        try:
+            prog.parse_options([], default_port=12345)
+        except:
+            self.fail("Unexpected exception when parsing options:\n%s"
+                      % "".join(traceback.format_exception(*sys.exc_info())))
+
+class TestOptionStack(unittest.TestCase):
+
+    def test(self):
+        parser = optparse.OptionParser()
+        parser.add_option("--port", type="int")
+        parser.add_option("--config-file")
+        parser.add_option("--log-file")
+        parser.set_defaults(port=1234)
+
+        default_opts = parser.get_default_values()
+        configfile_opts = optparse.Values()
+        commandline_opts, rest = parser.parse_args(["--config-file=/etc/cfg"],
+                                                   optparse.Values())
+        opts = OptionStack(default_opts, configfile_opts, commandline_opts)
+
+        opts.set_file_option("log_file", "/var/log/my.log")
+
+        self.assertEquals(opts.port, 1234)
+        self.assertEquals(opts.config_file, "/etc/cfg")
+        self.assertEquals(opts.log_file, "/var/log/my.log")


Property changes on: trunk/server/test/test_run.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/test/test_threadedserver.py
===================================================================
--- trunk/server/test/test_threadedserver.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_threadedserver.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,120 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for osaas.http.threadedserver"""
+
+import httplib
+from urlparse import urlparse
+from threading import Event
+
+from osaas.http.httpserver import HTTPRequestHandler
+from osaas.http.threadedserver import ThreadedHTTPServer
+
+from serversupport import ServerTest
+
+class BlockingRequestHandler(HTTPRequestHandler):
+
+    """RequestHandler that blocks on event object provided by the test case.
+    See the actual test methods for documentation on how the tests and
+    the request handler interact
+    """
+
+    def do_GET(self):
+        try:
+            events = self.server.test_case.events[self.path]
+        except KeyError:
+            self.send_error(404)
+            return
+
+        events[0].wait()
+
+        try:
+            self.send_response(200)
+            self.end_headers()
+            self.wfile.write(self.path)
+        finally:
+            # set events[1] in a finally block to make user it is always
+            # set.  Also, it must be set after the response has been
+            # sent.  Otherwise it can happen that the log entry is not
+            # written until the another thread has written a log entry
+            # which can make the test cases fail
+            events[1].set()
+
+
+class SimpleHTTPServerTests(ServerTest):
+
+    def setUp(self):
+        self.workerpool_log_stream = self.new_logger(self.error_logger_name
+                                                     + ".workerpool")
+        ServerTest.setUp(self)
+
+    def create_server(self, *args, **kw):
+        return ThreadedHTTPServer(RequestHandlerClass=BlockingRequestHandler,
+                                  *args, **kw)
+
+    def test_get_multiple(self):
+        """Test that multiple concurrent requests can be handled out of order"""
+        # The test basically sends two requests to the server and tests
+        # whether they have been processed in reverse order.  For this
+        # to work the test has to cooperate with the request handlers to
+        # make sure the first request is accepted by the serve but not
+        # actually processed until the second request has been
+        # processed.  This is made more complex by the desire to make
+        # sure that the test runs to completion even if the server
+        # implementation can only process requests in order of arrival.
+        #
+        # Even if the server only processes requests in order, the
+        # server and therefore the handlers run in their own thread but
+        # in the same process as the test case, so we can use lock based
+        # thread synchronization mechanisms.  In this case we use Event
+        # objects stored in the test case's instance variable events.
+        # The handlers can access the test case object through the serve
+        # object (see setUp).
+        #
+        # The events attribute is a dictionary mapping the paths used in
+        # HTTP GET requests to pairs of Event objects.  The test case
+        # creates two such pairs and sends corresponding GET requests to
+        # the server.  The handlers block on the first event object of
+        # the relevant pair.  The test case signals the first event of
+        # the request sent last and then waits for the second event with
+        # a timeout.  The requests will signal the second event after
+        # the first event has been signaled.  Normally, if the events
+        # are processed in parallel, the request startet second will
+        # already be waiting for the event and immediatly signal the
+        # other event.  If the requests are processed in order, the test
+        # case will wait until timeout.  In both cases the test case
+        # then signals the event of the first request.  Afterwards the
+        # responses of both requests are read and the log file is
+        # checked whether the requests were actually processed in the
+        # expected order.
+
+        self.events = {}
+        eventnames = ["/event1", "/event2"]
+
+        connections = {}
+        for name in eventnames:
+            self.events[name] = (Event(), Event())
+            http = httplib.HTTPConnection("localhost", self.server.server_port)
+            http.request("GET", name)
+            connections[name] = http
+
+        # unblock the second event first and then the first event and
+        # wait for it to report back.  The unblock the first event.
+        self.events[eventnames[1]][0].set()
+        self.events[eventnames[1]][1].wait(1.0)
+        self.events[eventnames[0]][0].set()
+
+        for name in eventnames:
+            http = connections[name]
+            response = http.getresponse()
+            self.assertEquals(response.status, 200)
+            self.assertEquals(response.read(), name)
+
+        self.check_log(['localhost - - [] "GET /event2 HTTP/1.1" 200 -',
+                        'localhost - - [] "GET /event1 HTTP/1.1" 200 -'])
+        self.assertEquals(self.workerpool_log_stream.getvalue(),
+                          "Starting 5 threads\n")


Property changes on: trunk/server/test/test_threadedserver.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/test/test_threadpool.py
===================================================================
--- trunk/server/test/test_threadpool.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_threadpool.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,65 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for ThreadPool"""
+
+from __future__ import nested_scopes
+
+import logging
+import unittest
+
+from serversupport import LoggerMixin
+
+from osaas.http.threadpool import ThreadPool
+
+class TestThreadPool(unittest.TestCase, LoggerMixin):
+
+    def setUp(self):
+        self.log_stream = self.new_logger("threadpool",
+                               format="%(levelname)s %(name)s %(message)s")
+        self.logger = logging.getLogger("threadpool")
+
+    def test_threadpool_start_stop_logging(self):
+        def handler(item):
+            pass
+        pool = ThreadPool(2, handler, logger=self.logger)
+        pool.start()
+        pool.stop()
+        self.assertEquals(self.log_stream.getvalue(),
+                          "INFO threadpool Starting 2 threads\n"
+                          "INFO threadpool Stopping threads\n")
+
+    def test_threadpool_error_handling(self):
+        # If the handler function of a thread pool raises an exception,
+        # the thread must not stop.  It must continue to take items out
+        # of the thread pool's queue and process them.  To test this we
+        # use a handler function that records whether it has been called
+        # and then raises an exception.  We put more items into the
+        # thread pool's queue than there are threads in the queue.  In
+        # the end, after the thread pool has been stopped, the handler
+        # function must have been called once for each item put into the
+        # queue.
+        record = []
+        def error(item):
+            record.append(item)
+            raise RuntimeError
+
+        pool = ThreadPool(2, error, logger=self.logger)
+        pool.start()
+
+        for i in range(3):
+            pool.put(i)
+
+        pool.stop()
+        self.assertEquals(sorted(record), range(3))
+
+        # rudimentary check that the errors were logged.
+        self.log_stream.seek(0)
+        self.assertEquals([line.strip() for line in self.log_stream.readlines()
+                           if line.startswith("ERROR")],
+                          ["ERROR threadpool Exception raised by"
+                           " threadpool handler function"] * 3)


Property changes on: trunk/server/test/test_threadpool.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native

Added: trunk/server/test/test_userdb.py
===================================================================
--- trunk/server/test/test_userdb.py	2007-08-27 08:50:46 UTC (rev 1)
+++ trunk/server/test/test_userdb.py	2007-08-27 11:09:41 UTC (rev 2)
@@ -0,0 +1,83 @@
+# Copyright (C) 2007 by Intevation GmbH
+# Authors:
+# Bernhard Herzog <bh at intevation.de>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with the software for details.
+
+"""Tests for UserDB"""
+
+import sys
+import os
+import unittest
+import traceback
+
+from support import FileTestMixin
+
+from osaas.userdb import UserDB, encode_ssha, check_password
+
+
+class TestSSHA1(unittest.TestCase, FileTestMixin):
+
+    def test_encode_ssha(self):
+        self.assertEquals(encode_ssha("secret", "salz"),
+                          "{SSHA}uGAUfJr9yps6jrPoJXwpSFb+ndxzYWx6")
+
+    def test_check_password_ssha_correct_password(self):
+        self.failUnless(check_password("secret",
+                                     "{SSHA}uGAUfJr9yps6jrPoJXwpSFb+ndxzYWx6"))
+
+    def test_check_password_ssha_wrong_password(self):
+        self.failIf(check_password("secret",
+                                   "{SSHA}ki+V0/wTlQFLv2lL1RShgx5O1EFzYWx6"))
+
+class TestUserDB(unittest.TestCase):
+
+    def test_valid_password(self):
+        db = UserDB([("user", "geheim")])
+        self.failUnless(db.check_credentials("user", "geheim"))
+
+    def test_invalid_password(self):
+        db = UserDB([("user", "geheim")])
+        self.failIf(db.check_credentials("user", "geraten"))
+
+    def test_unknown_user(self):
+        db = UserDB([("user", "geheim")])
+        self.failIf(db.check_credentials("irgendwer", "geraten"))
+
+    def test_add_user(self):
+        db = UserDB([])
+        self.failIf(db.check_credentials("arthur", "42"))
+        db.add_user("arthur", "42")
+        self.failUnless(db.check_credentials("arthur", "42"))
+
+
+class TestUserDBFileIO(unittest.TestCase, FileTestMixin):
+
+    contents = """\
+hans:{SSHA}l7c79AuF2IRrdNUBBCDH6Xs3adpsaW51eA==
+karl:{SSHA}uGAUfJr9yps6jrPoJXwpSFb+ndxzYWx6
+"""
+
+    def setUp(self):
+        self.filename = self.create_temp_file(self.id(), self.contents)
+        self.userdb = UserDB()
+        self.userdb.read(self.filename)
+
+    def test_reading(self):
+        self.failUnless(self.userdb.check_credentials("hans", "tux"))
+        self.failUnless(self.userdb.check_credentials("karl", "secret"))
+
+    def test_reading_and_writing(self):
+        filename = self.temp_file_name(self.id() + "-write")
+        self.userdb.write(filename)
+        self.assertEquals(open(filename).read(), self.contents)
+
+    def test_nonexisting_file(self):
+        os.remove(self.filename)
+        db = UserDB()
+        try:
+            db.read(self.filename)
+        except:
+            self.fail("Error when opening non-existing file:\n%s"
+                      % "".join(traceback.format_exception(*sys.exc_info())))


Property changes on: trunk/server/test/test_userdb.py
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + native



More information about the Osaas-commits mailing list