Coverage for gws-app/gws/test/mockserver.py: 0%
122 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1"""Test web server.
3This server runs in a dedicated docker container during testing
4and acts as a mock for our http-related functionality.
6This server does almost nothing by default, but the client can "extend" it by providing "snippets".
7A snippet is a Python code fragment, which is injected directly into the request handler.
9The properties of the request handler (like ``path``) are available as variables in snippets.
11With ``return end(content, status, **headers)`` the snippet can return an HTTP response to the client.
13When a request arrives, all snippets added so far are executed until one of them returns.
15The server understands the following POST requests:
17- ``/__add`` reads a snippet from the request body and adds it to the request handler
18- ``/__del`` removes all snippets so far
19- ``/__set`` removes all and add this one
21IT IS AN EXTREMELY BAD IDEA TO RUN THIS SERVER OUTSIDE OF A TEST ENVIRONMENT.
23Example of use::
25 # set up a snippet
27 requests.post('http://mock-server/__add', data=r'''
28 if path == '/say-hello' and query.get('x') == 'y':
29 return end('HELLO')
30 ''')
32 # invoke it
34 res = requests.get('http://mock-server/say-hello?x=y')
35 assert res.text == 'HELLO'
37The mockserver runs in a GWS container, so all gws modules are available for import.
39"""
41import sys
42import http.server
43import signal
44import json
45import urllib.parse
47import gws
49_SNIPPETS = []
52class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
53 body: bytes
54 """Raw request body."""
55 json: dict
56 """Request body decoded as json."""
57 method: str
58 """GET, POST etc."""
59 path: str
60 """Url path part."""
61 protocol_version = 'HTTP/1.1'
62 """Protocol version."""
63 query2: dict
64 """Query string as a key => [values] dict, e.g. ``{'a': ['1', '2'], ...etc}`` """
65 query: dict
66 """Query string as a key => value dict, e.g. ``{'a': '1', 'b': '2', ...etc}`` """
67 remote_host: str
68 """Remote host."""
69 remote_port: int
70 """Remote post."""
71 text: str
72 """Request body decoded as utf8."""
74 def handle_one_request(self):
75 try:
76 return super().handle_one_request()
77 except Exception as exc:
78 _writeln(f'[mockserver] SERVER ERROR: {exc!r}')
80 def do_GET(self):
81 self.method = 'GET'
82 self.prepare(b'')
83 if self.path == '/':
84 return self.end('OK')
85 return self.run_snippets()
87 def do_POST(self):
88 self.method = 'POST'
89 content_length = int(self.headers['Content-Length'])
90 self.prepare(self.rfile.read(content_length))
92 if self.path == '/__add':
93 _SNIPPETS.insert(0, _dedent(self.text))
94 return self.end('ok')
95 if self.path == '/__del':
96 _SNIPPETS[::] = []
97 return self.end('ok')
98 if self.path == '/__set':
99 _SNIPPETS[::] = []
100 _SNIPPETS.insert(0, _dedent(self.text))
101 return self.end('ok')
103 return self.run_snippets()
105 def prepare(self, body: bytes):
106 self.body = body
107 try:
108 self.text = self.body.decode('utf8')
109 except:
110 self.text = ''
111 try:
112 self.json = json.loads(self.text)
113 except:
114 self.json = {}
116 path, _, qs = self.path.partition('?')
117 self.path = path
118 self.query = {}
119 self.query2 = {}
120 if qs:
121 self.query2 = urllib.parse.parse_qs(qs)
122 self.query = {k: v[0] for k, v in self.query2.items()}
124 self.remote_host, self.remote_port = self.client_address
126 def run_snippets(self):
127 code = '\n'.join([
128 'def F():',
129 _indent('\n'.join(_SNIPPETS)),
130 _indent('return end("?", 404)'),
131 'F()'
132 ])
133 ctx = {**vars(self), 'end': self.end, 'gws': gws}
134 try:
135 exec(code, ctx)
136 except Exception as exc:
137 _writeln(f'[mockserver] SNIPPET ERROR: {exc!r}')
138 return self.end('Internal Server Error', 500)
140 def end(self, content, status=200, **headers):
141 hs = {k.lower(): v for k, v in headers.items()}
142 ct = hs.pop('content-type', '')
144 if isinstance(content, (list, dict)):
145 body = json.dumps(content).encode('utf8')
146 ct = ct or 'application/json'
147 elif isinstance(content, str):
148 body = content.encode('utf8')
149 ct = ct or 'text/plain'
150 else:
151 assert isinstance(content, bytes)
152 body = content
153 ct = ct or 'application/octet-stream'
155 hs['content-type'] = ct
156 hs['content-length'] = str(len(body))
158 self.send_response(status)
160 for k, v in hs.items():
161 self.send_header(k, v)
162 self.end_headers()
164 self.wfile.write(body)
167def _dedent(s):
168 ls = [p.rstrip() for p in s.split('\n')]
169 ind = 100_000
171 for ln in ls:
172 n = len(ln.lstrip())
173 if n > 0:
174 ind = min(ind, len(ln) - n)
176 return '\n'.join(ln[ind:] for ln in ls)
179def _indent(s):
180 ind = ' ' * 4
181 return '\n'.join(ind + ln for ln in s.split('\n'))
184def _writeln(s):
185 sys.stdout.write(s + '\n')
186 sys.stdout.flush()
189def main():
190 host = '0.0.0.0'
191 port = 80
193 httpd = http.server.ThreadingHTTPServer((host, port), HTTPRequestHandler)
194 signal.signal(signal.SIGTERM, lambda x, y: httpd.shutdown())
195 _writeln(f'[mockserver] started on {host}:{port}')
196 httpd.serve_forever()
199if __name__ == '__main__':
200 main()