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

1"""Test web server. 

2 

3This server runs in a dedicated docker container during testing 

4and acts as a mock for our http-related functionality. 

5 

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. 

8 

9The properties of the request handler (like ``path``) are available as variables in snippets. 

10 

11With ``return end(content, status, **headers)`` the snippet can return an HTTP response to the client. 

12 

13When a request arrives, all snippets added so far are executed until one of them returns. 

14 

15The server understands the following POST requests: 

16 

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 

20 

21IT IS AN EXTREMELY BAD IDEA TO RUN THIS SERVER OUTSIDE OF A TEST ENVIRONMENT. 

22 

23Example of use:: 

24 

25 # set up a snippet 

26 

27 requests.post('http://mock-server/__add', data=r''' 

28 if path == '/say-hello' and query.get('x') == 'y': 

29 return end('HELLO') 

30 ''') 

31 

32 # invoke it 

33 

34 res = requests.get('http://mock-server/say-hello?x=y') 

35 assert res.text == 'HELLO' 

36 

37The mockserver runs in a GWS container, so all gws modules are available for import. 

38 

39""" 

40 

41import sys 

42import http.server 

43import signal 

44import json 

45import urllib.parse 

46 

47import gws 

48 

49_SNIPPETS = [] 

50 

51 

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.""" 

73 

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}') 

79 

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() 

86 

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)) 

91 

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') 

102 

103 return self.run_snippets() 

104 

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 = {} 

115 

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()} 

123 

124 self.remote_host, self.remote_port = self.client_address 

125 

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) 

139 

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', '') 

143 

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' 

154 

155 hs['content-type'] = ct 

156 hs['content-length'] = str(len(body)) 

157 

158 self.send_response(status) 

159 

160 for k, v in hs.items(): 

161 self.send_header(k, v) 

162 self.end_headers() 

163 

164 self.wfile.write(body) 

165 

166 

167def _dedent(s): 

168 ls = [p.rstrip() for p in s.split('\n')] 

169 ind = 100_000 

170 

171 for ln in ls: 

172 n = len(ln.lstrip()) 

173 if n > 0: 

174 ind = min(ind, len(ln) - n) 

175 

176 return '\n'.join(ln[ind:] for ln in ls) 

177 

178 

179def _indent(s): 

180 ind = ' ' * 4 

181 return '\n'.join(ind + ln for ln in s.split('\n')) 

182 

183 

184def _writeln(s): 

185 sys.stdout.write(s + '\n') 

186 sys.stdout.flush() 

187 

188 

189def main(): 

190 host = '0.0.0.0' 

191 port = 80 

192 

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() 

197 

198 

199if __name__ == '__main__': 

200 main()