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