edq.testing.httpserver

  1import os
  2import typing
  3
  4import requests
  5
  6import edq.net.exchange
  7import edq.net.exchangeserver
  8import edq.net.request
  9import edq.testing.unittest
 10
 11class HTTPServerTest(edq.testing.unittest.BaseTest):
 12    """
 13    A unit test class that requires a testing HTTP server to be running.
 14    """
 15
 16    server_key: str = ''
 17    """
 18    A key to indicate which test server this test class is using.
 19    By default all test classes share the same server,
 20    but child classes can set this if they want to control who is using the same server.
 21    If `tear_down_server` is true, then the relevant server will be stopped (and removed) on a call to tearDownClass(),
 22    which happens after a test class is complete.
 23    """
 24
 25    tear_down_server: bool = True
 26    """
 27    Tear down the relevant test server in tearDownClass().
 28    If set to false then the server will never get torn down,
 29    but can be shared between child test classes.
 30    """
 31
 32    skip_test_exchanges_base: bool = False
 33    """ Skip test_exchanges_base. """
 34
 35    override_server_url: typing.Union[str, None] = None
 36    """ If set, return this URL from get_server_url(). """
 37
 38    _servers: typing.Dict[str, edq.net.exchangeserver.HTTPExchangeServer] = {}
 39    """ The active test servers. """
 40
 41    _complete_exchange_tests: typing.Set[str] = set()
 42    """
 43    Keep track of the servers (by key) that have run their test_exchanges_base.
 44    This test should only be run once per server.
 45    """
 46
 47    _child_class_setup_called: bool = False
 48    """ Keep track if the child class setup was called. """
 49
 50    @classmethod
 51    def setUpClass(cls) -> None:
 52        super().setUpClass()
 53
 54        if (not cls._child_class_setup_called):
 55            cls.child_class_setup()
 56            cls._child_class_setup_called = True
 57
 58        if (cls.server_key in cls._servers):
 59            return
 60
 61        server = cls.create_server()
 62        cls._servers[cls.server_key] = server
 63
 64        cls.setup_server(server)
 65        server.start()
 66        cls.post_start_server(server)
 67
 68    @classmethod
 69    def create_server(cls) -> edq.net.exchangeserver.HTTPExchangeServer:
 70        """ Create the actual exchange server. """
 71
 72        return edq.net.exchangeserver.HTTPExchangeServer()
 73
 74    @classmethod
 75    def tearDownClass(cls) -> None:
 76        super().tearDownClass()
 77
 78        if (cls.server_key not in cls._servers):
 79            return
 80
 81        server = cls.get_server()
 82
 83        if (cls.tear_down_server):
 84            server.stop()
 85            del cls._servers[cls.server_key]
 86            cls._complete_exchange_tests.discard(cls.server_key)
 87
 88    @classmethod
 89    def suite_cleanup(cls) -> None:
 90        """ Cleanup all test servers. """
 91
 92        for server in cls._servers.values():
 93            server.stop()
 94
 95        cls._servers.clear()
 96
 97    @classmethod
 98    def get_server(cls) -> edq.net.exchangeserver.HTTPExchangeServer:
 99        """ Get the current HTTP server or raise if there is no server. """
100
101        server = cls._servers.get(cls.server_key, None)
102        if (server is None):
103            raise ValueError("Server has not been initialized.")
104
105        return server
106
107    @classmethod
108    def child_class_setup(cls) -> None:
109        """ This function is the recommended time for child classes to set any configuration. """
110
111    @classmethod
112    def setup_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None:
113        """ An opportunity for child classes to configure the test server before starting it. """
114
115    @classmethod
116    def post_start_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None:
117        """ An opportunity for child classes to work with the server after it has been started, but before any tests. """
118
119    @classmethod
120    def get_server_url(cls) -> str:
121        """ Get the URL for this test's test server. """
122
123        if (cls.override_server_url is not None):
124            return cls.override_server_url
125
126        server = cls.get_server()
127
128        if (server.port is None):
129            raise ValueError("Test server port has not been set.")
130
131        return f"http://127.0.0.1:{server.port}"
132
133    def assert_exchange(self, request: edq.net.exchange.HTTPExchange, response: edq.net.exchange.HTTPExchange,
134            base_url: typing.Union[str, None] = None,
135            ) -> requests.Response:
136        """
137        Assert that the result of making the provided request matches the provided response.
138        The same HTTPExchange may be supplied for both the request and response.
139        By default, the server's URL will be used as the base URL.
140        The full response will be returned (if no assertion is raised).
141        """
142
143        server = self.get_server()
144
145        if (base_url is None):
146            base_url = self.get_server_url()
147
148        full_response, body = edq.net.request.make_with_exchange(request, base_url, raise_for_status = True, **server.match_options)
149
150        match, hint = response.match_response(full_response, override_body = body, **server.match_options)
151        if (not match):
152            raise AssertionError(f"Exchange does not match: '{hint}'.")
153
154        return full_response
155
156    def test_exchanges_base(self) -> None:
157        """ Test making a request with each of the loaded exchanges. """
158
159        # Check if this test has already been run for this server.
160        if (self.server_key in self._complete_exchange_tests):
161            # Don't skip the test (which will show up in the test output).
162            # Instead, just return.
163            return
164
165        if (self.skip_test_exchanges_base):
166            self.skipTest('test_exchanges_base has been manually skipped.')
167
168        self._complete_exchange_tests.add(self.server_key)
169
170        server = self.get_server()
171
172        for (i, exchange) in enumerate(server.get_exchanges()):
173            base_name = exchange.get_url()
174            if (exchange.source_path is not None):
175                base_name = os.path.splitext(os.path.basename(exchange.source_path))[0]
176
177            with self.subTest(msg = f"Case {i} ({base_name}):"):
178                self.assert_exchange(exchange, exchange)
class HTTPServerTest(edq.testing.unittest.BaseTest):
 12class HTTPServerTest(edq.testing.unittest.BaseTest):
 13    """
 14    A unit test class that requires a testing HTTP server to be running.
 15    """
 16
 17    server_key: str = ''
 18    """
 19    A key to indicate which test server this test class is using.
 20    By default all test classes share the same server,
 21    but child classes can set this if they want to control who is using the same server.
 22    If `tear_down_server` is true, then the relevant server will be stopped (and removed) on a call to tearDownClass(),
 23    which happens after a test class is complete.
 24    """
 25
 26    tear_down_server: bool = True
 27    """
 28    Tear down the relevant test server in tearDownClass().
 29    If set to false then the server will never get torn down,
 30    but can be shared between child test classes.
 31    """
 32
 33    skip_test_exchanges_base: bool = False
 34    """ Skip test_exchanges_base. """
 35
 36    override_server_url: typing.Union[str, None] = None
 37    """ If set, return this URL from get_server_url(). """
 38
 39    _servers: typing.Dict[str, edq.net.exchangeserver.HTTPExchangeServer] = {}
 40    """ The active test servers. """
 41
 42    _complete_exchange_tests: typing.Set[str] = set()
 43    """
 44    Keep track of the servers (by key) that have run their test_exchanges_base.
 45    This test should only be run once per server.
 46    """
 47
 48    _child_class_setup_called: bool = False
 49    """ Keep track if the child class setup was called. """
 50
 51    @classmethod
 52    def setUpClass(cls) -> None:
 53        super().setUpClass()
 54
 55        if (not cls._child_class_setup_called):
 56            cls.child_class_setup()
 57            cls._child_class_setup_called = True
 58
 59        if (cls.server_key in cls._servers):
 60            return
 61
 62        server = cls.create_server()
 63        cls._servers[cls.server_key] = server
 64
 65        cls.setup_server(server)
 66        server.start()
 67        cls.post_start_server(server)
 68
 69    @classmethod
 70    def create_server(cls) -> edq.net.exchangeserver.HTTPExchangeServer:
 71        """ Create the actual exchange server. """
 72
 73        return edq.net.exchangeserver.HTTPExchangeServer()
 74
 75    @classmethod
 76    def tearDownClass(cls) -> None:
 77        super().tearDownClass()
 78
 79        if (cls.server_key not in cls._servers):
 80            return
 81
 82        server = cls.get_server()
 83
 84        if (cls.tear_down_server):
 85            server.stop()
 86            del cls._servers[cls.server_key]
 87            cls._complete_exchange_tests.discard(cls.server_key)
 88
 89    @classmethod
 90    def suite_cleanup(cls) -> None:
 91        """ Cleanup all test servers. """
 92
 93        for server in cls._servers.values():
 94            server.stop()
 95
 96        cls._servers.clear()
 97
 98    @classmethod
 99    def get_server(cls) -> edq.net.exchangeserver.HTTPExchangeServer:
100        """ Get the current HTTP server or raise if there is no server. """
101
102        server = cls._servers.get(cls.server_key, None)
103        if (server is None):
104            raise ValueError("Server has not been initialized.")
105
106        return server
107
108    @classmethod
109    def child_class_setup(cls) -> None:
110        """ This function is the recommended time for child classes to set any configuration. """
111
112    @classmethod
113    def setup_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None:
114        """ An opportunity for child classes to configure the test server before starting it. """
115
116    @classmethod
117    def post_start_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None:
118        """ An opportunity for child classes to work with the server after it has been started, but before any tests. """
119
120    @classmethod
121    def get_server_url(cls) -> str:
122        """ Get the URL for this test's test server. """
123
124        if (cls.override_server_url is not None):
125            return cls.override_server_url
126
127        server = cls.get_server()
128
129        if (server.port is None):
130            raise ValueError("Test server port has not been set.")
131
132        return f"http://127.0.0.1:{server.port}"
133
134    def assert_exchange(self, request: edq.net.exchange.HTTPExchange, response: edq.net.exchange.HTTPExchange,
135            base_url: typing.Union[str, None] = None,
136            ) -> requests.Response:
137        """
138        Assert that the result of making the provided request matches the provided response.
139        The same HTTPExchange may be supplied for both the request and response.
140        By default, the server's URL will be used as the base URL.
141        The full response will be returned (if no assertion is raised).
142        """
143
144        server = self.get_server()
145
146        if (base_url is None):
147            base_url = self.get_server_url()
148
149        full_response, body = edq.net.request.make_with_exchange(request, base_url, raise_for_status = True, **server.match_options)
150
151        match, hint = response.match_response(full_response, override_body = body, **server.match_options)
152        if (not match):
153            raise AssertionError(f"Exchange does not match: '{hint}'.")
154
155        return full_response
156
157    def test_exchanges_base(self) -> None:
158        """ Test making a request with each of the loaded exchanges. """
159
160        # Check if this test has already been run for this server.
161        if (self.server_key in self._complete_exchange_tests):
162            # Don't skip the test (which will show up in the test output).
163            # Instead, just return.
164            return
165
166        if (self.skip_test_exchanges_base):
167            self.skipTest('test_exchanges_base has been manually skipped.')
168
169        self._complete_exchange_tests.add(self.server_key)
170
171        server = self.get_server()
172
173        for (i, exchange) in enumerate(server.get_exchanges()):
174            base_name = exchange.get_url()
175            if (exchange.source_path is not None):
176                base_name = os.path.splitext(os.path.basename(exchange.source_path))[0]
177
178            with self.subTest(msg = f"Case {i} ({base_name}):"):
179                self.assert_exchange(exchange, exchange)

A unit test class that requires a testing HTTP server to be running.

server_key: str = ''

A key to indicate which test server this test class is using. By default all test classes share the same server, but child classes can set this if they want to control who is using the same server. If tear_down_server is true, then the relevant server will be stopped (and removed) on a call to tearDownClass(), which happens after a test class is complete.

tear_down_server: bool = True

Tear down the relevant test server in tearDownClass(). If set to false then the server will never get torn down, but can be shared between child test classes.

skip_test_exchanges_base: bool = False

Skip test_exchanges_base.

override_server_url: Optional[str] = None

If set, return this URL from get_server_url().

@classmethod
def setUpClass(cls) -> None:
51    @classmethod
52    def setUpClass(cls) -> None:
53        super().setUpClass()
54
55        if (not cls._child_class_setup_called):
56            cls.child_class_setup()
57            cls._child_class_setup_called = True
58
59        if (cls.server_key in cls._servers):
60            return
61
62        server = cls.create_server()
63        cls._servers[cls.server_key] = server
64
65        cls.setup_server(server)
66        server.start()
67        cls.post_start_server(server)

Hook method for setting up class fixture before running tests in the class.

@classmethod
def create_server(cls) -> edq.net.exchangeserver.HTTPExchangeServer:
69    @classmethod
70    def create_server(cls) -> edq.net.exchangeserver.HTTPExchangeServer:
71        """ Create the actual exchange server. """
72
73        return edq.net.exchangeserver.HTTPExchangeServer()

Create the actual exchange server.

@classmethod
def tearDownClass(cls) -> None:
75    @classmethod
76    def tearDownClass(cls) -> None:
77        super().tearDownClass()
78
79        if (cls.server_key not in cls._servers):
80            return
81
82        server = cls.get_server()
83
84        if (cls.tear_down_server):
85            server.stop()
86            del cls._servers[cls.server_key]
87            cls._complete_exchange_tests.discard(cls.server_key)

Hook method for deconstructing the class fixture after running all tests in the class.

@classmethod
def suite_cleanup(cls) -> None:
89    @classmethod
90    def suite_cleanup(cls) -> None:
91        """ Cleanup all test servers. """
92
93        for server in cls._servers.values():
94            server.stop()
95
96        cls._servers.clear()

Cleanup all test servers.

@classmethod
def get_server(cls) -> edq.net.exchangeserver.HTTPExchangeServer:
 98    @classmethod
 99    def get_server(cls) -> edq.net.exchangeserver.HTTPExchangeServer:
100        """ Get the current HTTP server or raise if there is no server. """
101
102        server = cls._servers.get(cls.server_key, None)
103        if (server is None):
104            raise ValueError("Server has not been initialized.")
105
106        return server

Get the current HTTP server or raise if there is no server.

@classmethod
def child_class_setup(cls) -> None:
108    @classmethod
109    def child_class_setup(cls) -> None:
110        """ This function is the recommended time for child classes to set any configuration. """

This function is the recommended time for child classes to set any configuration.

@classmethod
def setup_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None:
112    @classmethod
113    def setup_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None:
114        """ An opportunity for child classes to configure the test server before starting it. """

An opportunity for child classes to configure the test server before starting it.

@classmethod
def post_start_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None:
116    @classmethod
117    def post_start_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None:
118        """ An opportunity for child classes to work with the server after it has been started, but before any tests. """

An opportunity for child classes to work with the server after it has been started, but before any tests.

@classmethod
def get_server_url(cls) -> str:
120    @classmethod
121    def get_server_url(cls) -> str:
122        """ Get the URL for this test's test server. """
123
124        if (cls.override_server_url is not None):
125            return cls.override_server_url
126
127        server = cls.get_server()
128
129        if (server.port is None):
130            raise ValueError("Test server port has not been set.")
131
132        return f"http://127.0.0.1:{server.port}"

Get the URL for this test's test server.

def assert_exchange( self, request: edq.net.exchange.HTTPExchange, response: edq.net.exchange.HTTPExchange, base_url: Optional[str] = None) -> requests.models.Response:
134    def assert_exchange(self, request: edq.net.exchange.HTTPExchange, response: edq.net.exchange.HTTPExchange,
135            base_url: typing.Union[str, None] = None,
136            ) -> requests.Response:
137        """
138        Assert that the result of making the provided request matches the provided response.
139        The same HTTPExchange may be supplied for both the request and response.
140        By default, the server's URL will be used as the base URL.
141        The full response will be returned (if no assertion is raised).
142        """
143
144        server = self.get_server()
145
146        if (base_url is None):
147            base_url = self.get_server_url()
148
149        full_response, body = edq.net.request.make_with_exchange(request, base_url, raise_for_status = True, **server.match_options)
150
151        match, hint = response.match_response(full_response, override_body = body, **server.match_options)
152        if (not match):
153            raise AssertionError(f"Exchange does not match: '{hint}'.")
154
155        return full_response

Assert that the result of making the provided request matches the provided response. The same HTTPExchange may be supplied for both the request and response. By default, the server's URL will be used as the base URL. The full response will be returned (if no assertion is raised).

def test_exchanges_base(self) -> None:
157    def test_exchanges_base(self) -> None:
158        """ Test making a request with each of the loaded exchanges. """
159
160        # Check if this test has already been run for this server.
161        if (self.server_key in self._complete_exchange_tests):
162            # Don't skip the test (which will show up in the test output).
163            # Instead, just return.
164            return
165
166        if (self.skip_test_exchanges_base):
167            self.skipTest('test_exchanges_base has been manually skipped.')
168
169        self._complete_exchange_tests.add(self.server_key)
170
171        server = self.get_server()
172
173        for (i, exchange) in enumerate(server.get_exchanges()):
174            base_name = exchange.get_url()
175            if (exchange.source_path is not None):
176                base_name = os.path.splitext(os.path.basename(exchange.source_path))[0]
177
178            with self.subTest(msg = f"Case {i} ({base_name}):"):
179                self.assert_exchange(exchange, exchange)

Test making a request with each of the loaded exchanges.