##// END OF EJS Templates
fix(tests): fixed callback deamon tests
super-admin -
r5467:912dc7a4 default
parent child Browse files
Show More
@@ -1,360 +1,363 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import logging
21 21 import io
22 22
23 23 import mock
24 24 import msgpack
25 25 import pytest
26 26 import tempfile
27 27
28 28 from rhodecode.lib.hook_daemon import http_hooks_deamon
29 29 from rhodecode.lib.hook_daemon import celery_hooks_deamon
30 30 from rhodecode.lib.hook_daemon import hook_module
31 31 from rhodecode.lib.hook_daemon import base as hook_base
32 32 from rhodecode.lib.str_utils import safe_bytes
33 33 from rhodecode.tests.utils import assert_message_in_log
34 34 from rhodecode.lib.ext_json import json
35 35
36 36 test_proto = http_hooks_deamon.HooksHttpHandler.MSGPACK_HOOKS_PROTO
37 37
38 38
39 39 class TestHooks(object):
40 40 def test_hooks_can_be_used_as_a_context_processor(self):
41 41 hooks = hook_module.Hooks()
42 42 with hooks as return_value:
43 43 pass
44 44 assert hooks == return_value
45 45
46 46
47 47 class TestHooksHttpHandler(object):
48 48 def test_read_request_parses_method_name_and_arguments(self):
49 49 data = {
50 50 'method': 'test',
51 51 'extras': {
52 52 'param1': 1,
53 53 'param2': 'a'
54 54 }
55 55 }
56 56 request = self._generate_post_request(data)
57 57 hooks_patcher = mock.patch.object(
58 58 hook_module.Hooks, data['method'], create=True, return_value=1)
59 59
60 60 with hooks_patcher as hooks_mock:
61 61 handler = http_hooks_deamon.HooksHttpHandler
62 62 handler.DEFAULT_HOOKS_PROTO = test_proto
63 63 handler.wbufsize = 10240
64 64 MockServer(handler, request)
65 65
66 66 hooks_mock.assert_called_once_with(data['extras'])
67 67
68 68 def test_hooks_serialized_result_is_returned(self):
69 69 request = self._generate_post_request({})
70 70 rpc_method = 'test'
71 71 hook_result = {
72 72 'first': 'one',
73 73 'second': 2
74 74 }
75 75 extras = {}
76 76
77 77 # patching our _read to return test method and proto used
78 78 read_patcher = mock.patch.object(
79 79 http_hooks_deamon.HooksHttpHandler, '_read_request',
80 80 return_value=(test_proto, rpc_method, extras))
81 81
82 82 # patch Hooks instance to return hook_result data on 'test' call
83 83 hooks_patcher = mock.patch.object(
84 84 hook_module.Hooks, rpc_method, create=True,
85 85 return_value=hook_result)
86 86
87 87 with read_patcher, hooks_patcher:
88 88 handler = http_hooks_deamon.HooksHttpHandler
89 89 handler.DEFAULT_HOOKS_PROTO = test_proto
90 90 handler.wbufsize = 10240
91 91 server = MockServer(handler, request)
92 92
93 93 expected_result = http_hooks_deamon.HooksHttpHandler.serialize_data(hook_result)
94 94
95 95 server.request.output_stream.seek(0)
96 96 assert server.request.output_stream.readlines()[-1] == expected_result
97 97
98 98 def test_exception_is_returned_in_response(self):
99 99 request = self._generate_post_request({})
100 100 rpc_method = 'test'
101 101
102 102 read_patcher = mock.patch.object(
103 103 http_hooks_deamon.HooksHttpHandler, '_read_request',
104 104 return_value=(test_proto, rpc_method, {}))
105 105
106 106 hooks_patcher = mock.patch.object(
107 107 hook_module.Hooks, rpc_method, create=True,
108 108 side_effect=Exception('Test exception'))
109 109
110 110 with read_patcher, hooks_patcher:
111 111 handler = http_hooks_deamon.HooksHttpHandler
112 112 handler.DEFAULT_HOOKS_PROTO = test_proto
113 113 handler.wbufsize = 10240
114 114 server = MockServer(handler, request)
115 115
116 116 server.request.output_stream.seek(0)
117 117 data = server.request.output_stream.readlines()
118 118 msgpack_data = b''.join(data[5:])
119 119 org_exc = http_hooks_deamon.HooksHttpHandler.deserialize_data(msgpack_data)
120 120 expected_result = {
121 121 'exception': 'Exception',
122 122 'exception_traceback': org_exc['exception_traceback'],
123 123 'exception_args': ['Test exception']
124 124 }
125 125 assert org_exc == expected_result
126 126
127 127 def test_log_message_writes_to_debug_log(self, caplog):
128 128 ip_port = ('0.0.0.0', 8888)
129 129 handler = http_hooks_deamon.HooksHttpHandler(MockRequest('POST /'), ip_port, mock.Mock())
130 130 fake_date = '1/Nov/2015 00:00:00'
131 131 date_patcher = mock.patch.object(
132 132 handler, 'log_date_time_string', return_value=fake_date)
133 133
134 134 with date_patcher, caplog.at_level(logging.DEBUG):
135 135 handler.log_message('Some message %d, %s', 123, 'string')
136 136
137 137 expected_message = f"HOOKS: client={ip_port} - - [{fake_date}] Some message 123, string"
138 138
139 139 assert_message_in_log(
140 140 caplog.records, expected_message,
141 141 levelno=logging.DEBUG, module='http_hooks_deamon')
142 142
143 143 def _generate_post_request(self, data, proto=test_proto):
144 144 if proto == http_hooks_deamon.HooksHttpHandler.MSGPACK_HOOKS_PROTO:
145 145 payload = msgpack.packb(data)
146 146 else:
147 147 payload = json.dumps(data)
148 148
149 149 return b'POST / HTTP/1.0\nContent-Length: %d\n\n%b' % (
150 150 len(payload), payload)
151 151
152 152
153 153 class ThreadedHookCallbackDaemon(object):
154 154 def test_constructor_calls_prepare(self):
155 155 prepare_daemon_patcher = mock.patch.object(
156 156 http_hooks_deamon.ThreadedHookCallbackDaemon, '_prepare')
157 157 with prepare_daemon_patcher as prepare_daemon_mock:
158 158 http_hooks_deamon.ThreadedHookCallbackDaemon()
159 159 prepare_daemon_mock.assert_called_once_with()
160 160
161 161 def test_run_is_called_on_context_start(self):
162 162 patchers = mock.patch.multiple(
163 163 http_hooks_deamon.ThreadedHookCallbackDaemon,
164 164 _run=mock.DEFAULT, _prepare=mock.DEFAULT, __exit__=mock.DEFAULT)
165 165
166 166 with patchers as mocks:
167 167 daemon = http_hooks_deamon.ThreadedHookCallbackDaemon()
168 168 with daemon as daemon_context:
169 169 pass
170 170 mocks['_run'].assert_called_once_with()
171 171 assert daemon_context == daemon
172 172
173 173 def test_stop_is_called_on_context_exit(self):
174 174 patchers = mock.patch.multiple(
175 175 http_hooks_deamon.ThreadedHookCallbackDaemon,
176 176 _run=mock.DEFAULT, _prepare=mock.DEFAULT, _stop=mock.DEFAULT)
177 177
178 178 with patchers as mocks:
179 179 daemon = http_hooks_deamon.ThreadedHookCallbackDaemon()
180 180 with daemon as daemon_context:
181 181 assert mocks['_stop'].call_count == 0
182 182
183 183 mocks['_stop'].assert_called_once_with()
184 184 assert daemon_context == daemon
185 185
186 186
187 187 class TestHttpHooksCallbackDaemon(object):
188 188 def test_hooks_callback_generates_new_port(self, caplog):
189 189 with caplog.at_level(logging.DEBUG):
190 190 daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881)
191 191 assert daemon._daemon.server_address == ('127.0.0.1', 8881)
192 192
193 193 with caplog.at_level(logging.DEBUG):
194 194 daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host=None, port=None)
195 195 assert daemon._daemon.server_address[1] in range(0, 66000)
196 196 assert daemon._daemon.server_address[0] != '127.0.0.1'
197 197
198 198 def test_prepare_inits_daemon_variable(self, tcp_server, caplog):
199 199 with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG):
200 200 daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881)
201 201 assert daemon._daemon == tcp_server
202 202
203 203 _, port = tcp_server.server_address
204 204
205 205 msg = f"HOOKS: 127.0.0.1:{port} Preparing HTTP callback daemon registering " \
206 206 f"hook object: <class 'rhodecode.lib.hook_daemon.http_hooks_deamon.HooksHttpHandler'>"
207 207 assert_message_in_log(
208 208 caplog.records, msg, levelno=logging.DEBUG, module='http_hooks_deamon')
209 209
210 210 def test_prepare_inits_hooks_uri_and_logs_it(
211 211 self, tcp_server, caplog):
212 212 with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG):
213 213 daemon = http_hooks_deamon.HttpHooksCallbackDaemon(host='127.0.0.1', port=8881)
214 214
215 215 _, port = tcp_server.server_address
216 216 expected_uri = '{}:{}'.format('127.0.0.1', port)
217 217 assert daemon.hooks_uri == expected_uri
218 218
219 219 msg = f"HOOKS: 127.0.0.1:{port} Preparing HTTP callback daemon registering " \
220 220 f"hook object: <class 'rhodecode.lib.hook_daemon.http_hooks_deamon.HooksHttpHandler'>"
221 221
222 222 assert_message_in_log(
223 223 caplog.records, msg,
224 224 levelno=logging.DEBUG, module='http_hooks_deamon')
225 225
226 226 def test_run_creates_a_thread(self, tcp_server):
227 227 thread = mock.Mock()
228 228
229 229 with self._tcp_patcher(tcp_server):
230 230 daemon = http_hooks_deamon.HttpHooksCallbackDaemon()
231 231
232 232 with self._thread_patcher(thread) as thread_mock:
233 233 daemon._run()
234 234
235 235 thread_mock.assert_called_once_with(
236 236 target=tcp_server.serve_forever,
237 237 kwargs={'poll_interval': daemon.POLL_INTERVAL})
238 238 assert thread.daemon is True
239 239 thread.start.assert_called_once_with()
240 240
241 241 def test_run_logs(self, tcp_server, caplog):
242 242
243 243 with self._tcp_patcher(tcp_server):
244 244 daemon = http_hooks_deamon.HttpHooksCallbackDaemon()
245 245
246 246 with self._thread_patcher(mock.Mock()), caplog.at_level(logging.DEBUG):
247 247 daemon._run()
248 248
249 249 assert_message_in_log(
250 250 caplog.records,
251 251 'Running thread-based loop of callback daemon in background',
252 252 levelno=logging.DEBUG, module='http_hooks_deamon')
253 253
254 254 def test_stop_cleans_up_the_connection(self, tcp_server, caplog):
255 255 thread = mock.Mock()
256 256
257 257 with self._tcp_patcher(tcp_server):
258 258 daemon = http_hooks_deamon.HttpHooksCallbackDaemon()
259 259
260 260 with self._thread_patcher(thread), caplog.at_level(logging.DEBUG):
261 261 with daemon:
262 262 assert daemon._daemon == tcp_server
263 263 assert daemon._callback_thread == thread
264 264
265 265 assert daemon._daemon is None
266 266 assert daemon._callback_thread is None
267 267 tcp_server.shutdown.assert_called_with()
268 268 thread.join.assert_called_once_with()
269 269
270 270 assert_message_in_log(
271 271 caplog.records, 'Waiting for background thread to finish.',
272 272 levelno=logging.DEBUG, module='http_hooks_deamon')
273 273
274 274 def _tcp_patcher(self, tcp_server):
275 275 return mock.patch.object(
276 276 http_hooks_deamon, 'TCPServer', return_value=tcp_server)
277 277
278 278 def _thread_patcher(self, thread):
279 279 return mock.patch.object(
280 280 http_hooks_deamon.threading, 'Thread', return_value=thread)
281 281
282 282
283 283 class TestPrepareHooksDaemon(object):
284 284
285 285 @pytest.mark.parametrize('protocol', ('celery',))
286 286 def test_returns_celery_hooks_callback_daemon_when_celery_protocol_specified(
287 287 self, protocol):
288 288 with tempfile.NamedTemporaryFile(mode='w') as temp_file:
289 289 temp_file.write("[app:main]\ncelery.broker_url = redis://redis/0\n"
290 290 "celery.result_backend = redis://redis/0")
291 291 temp_file.flush()
292 292 expected_extras = {'config': temp_file.name}
293 293 callback, extras = hook_base.prepare_callback_daemon(
294 294 expected_extras, protocol=protocol, host='')
295 295 assert isinstance(callback, celery_hooks_deamon.CeleryHooksCallbackDaemon)
296 296
297 297 @pytest.mark.parametrize('protocol, expected_class', (
298 298 ('http', http_hooks_deamon.HttpHooksCallbackDaemon),
299 299 ))
300 300 def test_returns_real_hooks_callback_daemon_when_protocol_is_specified(
301 301 self, protocol, expected_class):
302 302 expected_extras = {
303 303 'extra1': 'value1',
304 304 'txn_id': 'txnid2',
305 305 'hooks_protocol': protocol.lower(),
306 306 'task_backend': '',
307 307 'task_queue': '',
308 'repo_store': '/var/opt/rhodecode_repo_store'
308 'repo_store': '/var/opt/rhodecode_repo_store',
309 'repository': 'rhodecode',
309 310 }
311 from rhodecode import CONFIG
312 CONFIG['vcs.svn.redis_conn'] = 'redis://redis:6379/0'
310 313 callback, extras = hook_base.prepare_callback_daemon(
311 314 expected_extras.copy(), protocol=protocol, host='127.0.0.1',
312 315 txn_id='txnid2')
313 316 assert isinstance(callback, expected_class)
314 317 extras.pop('hooks_uri')
315 318 expected_extras['time'] = extras['time']
316 319 assert extras == expected_extras
317 320
318 321 @pytest.mark.parametrize('protocol', (
319 322 'invalid',
320 323 'Http',
321 324 'HTTP',
322 325 ))
323 326 def test_raises_on_invalid_protocol(self, protocol):
324 327 expected_extras = {
325 328 'extra1': 'value1',
326 329 'hooks_protocol': protocol.lower()
327 330 }
328 331 with pytest.raises(Exception):
329 332 callback, extras = hook_base.prepare_callback_daemon(
330 333 expected_extras.copy(),
331 334 protocol=protocol, host='127.0.0.1')
332 335
333 336
334 337 class MockRequest(object):
335 338
336 339 def __init__(self, request):
337 340 self.request = request
338 341 self.input_stream = io.BytesIO(safe_bytes(self.request))
339 342 self.output_stream = io.BytesIO() # make it un-closable for testing invesitagion
340 343 self.output_stream.close = lambda: None
341 344
342 345 def makefile(self, mode, *args, **kwargs):
343 346 return self.output_stream if mode == 'wb' else self.input_stream
344 347
345 348
346 349 class MockServer(object):
347 350
348 351 def __init__(self, handler_cls, request):
349 352 ip_port = ('0.0.0.0', 8888)
350 353 self.request = MockRequest(request)
351 354 self.server_address = ip_port
352 355 self.handler = handler_cls(self.request, ip_port, self)
353 356
354 357
355 358 @pytest.fixture()
356 359 def tcp_server():
357 360 server = mock.Mock()
358 361 server.server_address = ('127.0.0.1', 8881)
359 362 server.wbufsize = 1024
360 363 return server
General Comments 0
You need to be logged in to leave comments. Login now