0001-wscall-notify-and-record-errors-on-failure-44050.patch
tests/test_workflows.py | ||
---|---|---|
2368 | 2368 |
with mock.patch('wcs.wscalls.get_secret_and_orig') as mocked_secret_and_orig: |
2369 | 2369 |
mocked_secret_and_orig.return_value = ('secret', 'localhost') |
2370 | 2370 |
with mock.patch('wcs.qommon.misc._http_request') as mocked_http_post: |
2371 |
mocked_http_post.return_value = ('response', '200', 'data', 'headers')
|
|
2371 |
mocked_http_post.return_value = (mock.Mock(headers={}), 200, 'data', 'headers')
|
|
2372 | 2372 |
item.perform(formdata) |
2373 | 2373 |
url = mocked_http_post.call_args[0][0] |
2374 | 2374 |
payload = mocked_http_post.call_args[1]['body'] |
tests/test_wscall.py | ||
---|---|---|
1 | 1 |
import json |
2 | 2 |
import pytest |
3 | 3 | |
4 |
from wcs import fields |
|
5 |
from wcs.formdef import FormDef |
|
6 |
from wcs.logged_errors import LoggedError |
|
4 | 7 |
from wcs.qommon.http_request import HTTPRequest |
5 | 8 |
from wcs.qommon.template import Template |
6 | 9 |
from wcs.wscalls import NamedWsCall |
7 | 10 | |
8 |
from utilities import create_temporary_pub, clean_temporary_pub |
|
11 |
from utilities import get_app, create_temporary_pub, clean_temporary_pub
|
|
9 | 12 | |
10 | 13 | |
11 | 14 |
@pytest.fixture |
... | ... | |
165 | 168 |
pass |
166 | 169 |
assert http_requests.get_last('url') == wscall.request['url'] |
167 | 170 |
assert http_requests.get_last('method') == 'DELETE' |
171 | ||
172 | ||
173 |
@pytest.mark.parametrize('notify_on_errors', [True, False]) |
|
174 |
@pytest.mark.parametrize('record_on_errors', [True, False]) |
|
175 |
def test_webservice_on_error(http_requests, pub, emails, notify_on_errors, record_on_errors): |
|
176 |
pub.cfg['debug'] = {'error_email': 'errors@localhost.invalid'} |
|
177 |
pub.write_cfg() |
|
178 | ||
179 |
NamedWsCall.wipe() |
|
180 |
LoggedError.wipe() |
|
181 |
FormDef.wipe() |
|
182 | ||
183 |
wscall = NamedWsCall() |
|
184 |
wscall.name = 'Hello world' |
|
185 |
wscall.notify_on_errors = notify_on_errors |
|
186 |
wscall.record_on_errors = record_on_errors |
|
187 |
wscall.store() |
|
188 |
assert wscall.slug == 'hello_world' |
|
189 | ||
190 |
formdef = FormDef() |
|
191 |
formdef.name = 'foobar' |
|
192 |
formdef.fields = [ |
|
193 |
fields.CommentField(id='0', label='Foo Bar {{ webservice.hello_world }}', type='comment'), |
|
194 |
] |
|
195 |
formdef.store() |
|
196 | ||
197 |
for url_part in ['json', 'json-err0', 'json-errheader0']: |
|
198 |
wscall.request = {'url': 'http://remote.example.net/%s' % url_part} |
|
199 |
wscall.store() |
|
200 |
resp = get_app(pub).get('/foobar/') |
|
201 |
assert 'Foo Bar ' in resp.text |
|
202 |
assert emails.count() == 0 |
|
203 |
assert LoggedError.count() == 0 |
|
204 | ||
205 |
for url_part in ['400', '400-json', '404', '404-json', '500', 'json-err1', 'json-errheader1']: |
|
206 |
status_code = 200 |
|
207 |
if not url_part.startswith('json'): |
|
208 |
status_code = url_part[:3] |
|
209 |
wscall.request = {'url': 'http://remote.example.net/%s' % url_part} |
|
210 |
wscall.store() |
|
211 |
resp = get_app(pub).get('/foobar/') |
|
212 |
assert 'Foo Bar ' in resp.text |
|
213 |
if notify_on_errors: |
|
214 |
assert emails.count() == 1 |
|
215 |
assert "[ERROR] ['WSCALL'] Exception: %s whatever" % status_code in emails.emails |
|
216 |
emails.empty() |
|
217 |
else: |
|
218 |
assert emails.count() == 0 |
|
219 |
if record_on_errors: |
|
220 |
assert LoggedError.count() == 1 |
|
221 |
LoggedError.wipe() |
|
222 |
else: |
|
223 |
assert LoggedError.count() == 0 |
tests/test_wscalls_admin_pages.py | ||
---|---|---|
44 | 44 |
NamedWsCall.wipe() |
45 | 45 |
wscall = NamedWsCall(name='xxx') |
46 | 46 |
wscall.description = 'description' |
47 |
wscall.notify_on_errors = True |
|
48 |
wscall.record_on_errors = True |
|
47 | 49 |
wscall.request = { |
48 | 50 |
'url': 'http://remote.example.net/json', |
49 | 51 |
'request_signature_key': 'xxx', |
... | ... | |
55 | 57 |
return wscall |
56 | 58 | |
57 | 59 | |
58 |
def test_wscalls_new(pub): |
|
60 |
@pytest.mark.parametrize('value', [True, False]) |
|
61 |
def test_wscalls_new(pub, value): |
|
59 | 62 |
create_superuser(pub) |
60 | 63 |
NamedWsCall.wipe() |
61 | 64 |
app = login(get_app(pub)) |
... | ... | |
69 | 72 |
# go to the page and add a webservice call |
70 | 73 |
resp = app.get('/backoffice/settings/wscalls/') |
71 | 74 |
resp = resp.click('New webservice call') |
75 |
assert resp.form['notify_on_errors'].value == 'yes' |
|
76 |
assert resp.form['record_on_errors'].value == 'yes' |
|
72 | 77 |
resp.form['name'] = 'a new webservice call' |
73 | 78 |
resp.form['description'] = 'description' |
79 |
resp.form['notify_on_errors'] = value |
|
80 |
resp.form['record_on_errors'] = value |
|
74 | 81 |
resp.form['request$url'] = 'http://remote.example.net/json' |
75 | 82 |
resp = resp.form.submit('submit') |
76 | 83 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/' |
... | ... | |
82 | 89 |
assert 'Edit webservice call' in resp.text |
83 | 90 | |
84 | 91 |
assert NamedWsCall.get('a_new_webservice_call').name == 'a new webservice call' |
92 |
assert NamedWsCall.get('a_new_webservice_call').notify_on_errors == value |
|
93 |
assert NamedWsCall.get('a_new_webservice_call').record_on_errors == value |
|
85 | 94 | |
86 | 95 | |
87 | 96 |
def test_wscalls_view(pub, wscall): |
... | ... | |
100 | 109 |
resp = app.get('/backoffice/settings/wscalls/xxx/') |
101 | 110 |
resp = resp.click(href='edit') |
102 | 111 |
assert resp.form['name'].value == 'xxx' |
112 |
assert resp.form['notify_on_errors'].value == 'yes' |
|
113 |
assert resp.form['record_on_errors'].value == 'yes' |
|
103 | 114 |
assert 'slug' in resp.form.fields |
104 | 115 |
resp.form['description'] = 'bla bla bla' |
116 |
resp.form['notify_on_errors'] = False |
|
117 |
resp.form['record_on_errors'] = False |
|
105 | 118 |
resp = resp.form.submit('submit') |
106 | 119 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/xxx/' |
107 | 120 |
resp = resp.follow() |
108 | 121 | |
109 | 122 |
assert NamedWsCall.get('xxx').description == 'bla bla bla' |
123 |
assert NamedWsCall.get('xxx').notify_on_errors is False |
|
124 |
assert NamedWsCall.get('xxx').record_on_errors is False |
|
110 | 125 | |
111 | 126 |
resp = app.get('/backoffice/settings/wscalls/xxx/') |
112 | 127 |
resp = resp.click(href='edit') |
113 | 128 |
assert resp.form['name'].value == 'xxx' |
129 |
assert resp.form['notify_on_errors'].value is None |
|
130 |
assert resp.form['record_on_errors'].value is None |
|
114 | 131 |
assert 'slug' in resp.form.fields |
115 | 132 |
resp.form['slug'] = 'yyy' |
116 | 133 |
resp = resp.form.submit('submit') |
tests/utilities.py | ||
---|---|---|
324 | 324 | |
325 | 325 |
status, data, headers = { |
326 | 326 |
'http://remote.example.net/204': (204, None, None), |
327 |
'http://remote.example.net/400': (400, 'bad request', None), |
|
327 | 328 |
'http://remote.example.net/400-json': (400, '{"err": 1, "err_desc": ":("}', None), |
328 | 329 |
'http://remote.example.net/404': (404, 'page not found', None), |
329 | 330 |
'http://remote.example.net/404-json': (404, '{"err": 1}', None), |
wcs/admin/wscalls.py | ||
---|---|---|
52 | 52 |
value=self.wscall.request, |
53 | 53 |
title=_('Request'), required=True) |
54 | 54 | |
55 |
form.add( |
|
56 |
CheckboxWidget, |
|
57 |
'notify_on_errors', |
|
58 |
title=_('Notify on errors'), |
|
59 |
value=self.wscall.notify_on_errors if self.wscall.slug else True) |
|
60 |
form.add( |
|
61 |
CheckboxWidget, |
|
62 |
'record_on_errors', |
|
63 |
title=_('Record on errors'), |
|
64 |
value=self.wscall.record_on_errors if self.wscall.slug else True) |
|
55 | 65 |
if not self.wscall.is_readonly(): |
56 | 66 |
form.add_submit('submit', _('Submit')) |
57 | 67 |
form.add_submit('cancel', _('Cancel')) |
... | ... | |
76 | 86 | |
77 | 87 |
self.wscall.name = name |
78 | 88 |
self.wscall.description = form.get_widget('description').parse() |
89 |
self.wscall.notify_on_errors = form.get_widget('notify_on_errors').parse() |
|
90 |
self.wscall.record_on_errors = form.get_widget('record_on_errors').parse() |
|
79 | 91 |
self.wscall.request = form.get_widget('request').parse() |
80 | 92 |
if self.wscall.slug: |
81 | 93 |
self.wscall.slug = slug |
wcs/publisher.py | ||
---|---|---|
317 | 317 |
conn.commit() |
318 | 318 |
cur.close() |
319 | 319 | |
320 |
def notify_of_exception(self, exc_tuple, context=None): |
|
320 |
def notify_of_exception(self, exc_tuple, context=None, record=True, notify=True):
|
|
321 | 321 |
exc_type, exc_value, tb = exc_tuple |
322 | 322 |
error_summary = traceback.format_exception_only(exc_type, exc_value) |
323 |
error_summary = error_summary[0][0:-1] # de-listify and strip newline |
|
323 |
error_summary = error_summary[0][0:-1] # de-listify and strip newline
|
|
324 | 324 |
if context: |
325 | 325 |
error_summary = '%s %s' % (context, error_summary) |
326 | 326 | |
... | ... | |
330 | 330 |
exc_type, exc_value, |
331 | 331 |
tb)) |
332 | 332 | |
333 |
self.log_internal_error(error_summary, plain_error_msg, record=True)
|
|
333 |
self.log_internal_error(error_summary, plain_error_msg, record=record, notify=notify)
|
|
334 | 334 | |
335 |
def log_internal_error(self, error_summary, plain_error_msg, record=False): |
|
335 |
def log_internal_error(self, error_summary, plain_error_msg, record=False, notify=True):
|
|
336 | 336 |
tech_id = None |
337 | 337 |
if record: |
338 | 338 |
logged_exception = LoggedError.record_exception( |
339 | 339 |
error_summary, plain_error_msg, publisher=self) |
340 | 340 |
if logged_exception: |
341 | 341 |
tech_id = logged_exception.tech_id |
342 |
if not notify: |
|
343 |
return |
|
342 | 344 |
try: |
343 | 345 |
self.logger.log_internal_error(error_summary, plain_error_msg, tech_id) |
344 | 346 |
except socket.error: |
wcs/templates/wcs/backoffice/wscall.html | ||
---|---|---|
33 | 33 |
<li>{% trans "Method:" %} {% if wscall.request.method == 'POST' %}POST{% else %}GET{% endif %}</li> |
34 | 34 |
</ul> |
35 | 35 |
</div> |
36 | ||
37 |
<div class="bo-block"> |
|
38 |
<ul> |
|
39 |
<li>{% trans "Notify on errors:" %} {{ wscall.notify_on_errors|yesno }}</li> |
|
40 |
<li>{% trans "Record on errors:" %} {{ wscall.record_on_errors|yesno }}</li> |
|
41 |
</ul> |
|
42 |
</div> |
|
36 | 43 |
{% endblock %} |
wcs/wf/wscall.py | ||
---|---|---|
33 | 33 |
from ..qommon.misc import json_loads |
34 | 34 |
from wcs.workflows import (WorkflowStatusItem, register_item_class, |
35 | 35 |
AbortActionException, AttachmentEvolutionPart) |
36 |
from wcs.wscalls import call_webservice |
|
36 |
from wcs.wscalls import call_webservice, get_app_error_code
|
|
37 | 37 | |
38 | 38 | |
39 | 39 |
class JournalWsCallErrorPart: #pylint: disable=C1001 |
... | ... | |
306 | 306 |
exc_info=sys.exc_info()) |
307 | 307 |
return |
308 | 308 | |
309 |
app_error_code = 0
|
|
309 |
app_error_code = get_app_error_code(response, data, self.response_type)
|
|
310 | 310 |
app_error_code_header = response.headers.get('x-error-code') |
311 |
if app_error_code_header: |
|
312 |
# result is good only if header value is '0' |
|
313 |
try: |
|
314 |
app_error_code = int(app_error_code_header) |
|
315 |
except ValueError as e: |
|
316 |
app_error_code = app_error_code_header |
|
317 |
elif self.response_type == 'json': |
|
318 |
try: |
|
319 |
d = json_loads(data) |
|
320 |
except (ValueError, TypeError) as e: |
|
321 |
pass |
|
322 |
else: |
|
323 |
if isinstance(d, dict) and d.get('err'): |
|
324 |
app_error_code = d['err'] |
|
325 | 311 | |
326 | 312 |
if self.varname: |
327 | 313 |
workflow_data.update({ |
wcs/wscalls.py | ||
---|---|---|
28 | 28 |
from .qommon import _, force_str |
29 | 29 |
from .qommon.misc import simplify, get_variadic_url, JSONEncoder, json_loads |
30 | 30 |
from .qommon.xml_storage import XmlStorableObject |
31 |
from .qommon.form import (CompositeWidget, StringWidget, WidgetDict, |
|
32 |
ComputedExpressionWidget, RadiobuttonsWidget, CheckboxWidget) |
|
31 |
from .qommon.form import ( |
|
32 |
CompositeWidget, StringWidget, WidgetDict, |
|
33 |
ComputedExpressionWidget, RadiobuttonsWidget, CheckboxWidget) |
|
33 | 34 |
from .qommon import misc |
34 | 35 |
from .qommon.template import Template |
35 | 36 | |
... | ... | |
37 | 38 |
from wcs.workflows import WorkflowStatusItem |
38 | 39 | |
39 | 40 | |
40 |
def call_webservice(url, qs_data=None, request_signature_key=None, |
|
41 |
def get_app_error_code(response, data, response_type): |
|
42 |
app_error_code = 0 |
|
43 |
app_error_code_header = response.headers.get('x-error-code') |
|
44 |
if app_error_code_header: |
|
45 |
# result is good only if header value is '0' |
|
46 |
try: |
|
47 |
app_error_code = int(app_error_code_header) |
|
48 |
except ValueError: |
|
49 |
app_error_code = app_error_code_header |
|
50 |
elif response_type == 'json': |
|
51 |
try: |
|
52 |
d = json_loads(data) |
|
53 |
except (ValueError, TypeError): |
|
54 |
pass |
|
55 |
else: |
|
56 |
if isinstance(d, dict) and d.get('err'): |
|
57 |
app_error_code = d['err'] |
|
58 |
return app_error_code |
|
59 | ||
60 | ||
61 |
def call_webservice( |
|
62 |
url, qs_data=None, request_signature_key=None, |
|
41 | 63 |
method=None, post_data=None, post_formdata=None, formdata=None, |
42 |
cache=False, **kwargs): |
|
64 |
cache=False, notify_on_errors=False, record_on_errors=False, **kwargs):
|
|
43 | 65 | |
44 | 66 |
url = url.strip() |
45 | 67 |
if Template.is_template_string(url): |
... | ... | |
122 | 144 |
if cache is True and request and hasattr(request, 'wscalls_cache'): |
123 | 145 |
request.wscalls_cache[unsigned_url] = (status, data) |
124 | 146 | |
147 |
app_error_code = get_app_error_code(response, data, 'json') |
|
148 | ||
149 |
if (app_error_code != 0 or status >= 400) and (notify_on_errors or record_on_errors): |
|
150 |
summary = '<no response>' |
|
151 |
if response is not None: |
|
152 |
summary = '%s %s' % (status, response.reason) |
|
153 |
try: |
|
154 |
raise Exception(summary) |
|
155 |
except Exception: |
|
156 |
exc_info = sys.exc_info() |
|
157 |
get_publisher().notify_of_exception( |
|
158 |
exc_info, context=['WSCALL'], notify=notify_on_errors, record=record_on_errors) |
|
159 | ||
125 | 160 |
return (response, status, data) |
126 | 161 | |
127 | 162 | |
... | ... | |
133 | 168 |
slug = None |
134 | 169 |
description = None |
135 | 170 |
request = None |
171 |
notify_on_errors = False |
|
172 |
record_on_errors = False |
|
136 | 173 | |
137 | 174 |
# declarations for serialization |
138 |
XML_NODES = [('name', 'str'), ('slug', 'str'), ('description', 'str'), |
|
139 |
('request', 'request'),] |
|
175 |
XML_NODES = [ |
|
176 |
('name', 'str'), |
|
177 |
('slug', 'str'), |
|
178 |
('description', 'str'), |
|
179 |
('request', 'request'), |
|
180 |
('notify_on_errors', 'bool'), |
|
181 |
('record_on_errors', 'bool'), |
|
182 |
] |
|
140 | 183 | |
141 | 184 |
def __init__(self, name=None): |
142 | 185 |
XmlStorableObject.__init__(self) |
... | ... | |
205 | 248 |
return {'webservice': WsCallsSubstitutionProxy()} |
206 | 249 | |
207 | 250 |
def call(self): |
208 |
(response, status, data) = call_webservice(cache=True, **self.request) |
|
251 |
(response, status, data) = call_webservice( |
|
252 |
cache=True, |
|
253 |
notify_on_errors=self.notify_on_errors, |
|
254 |
record_on_errors=self.record_on_errors, |
|
255 |
**self.request) |
|
209 | 256 |
return json_loads(data) |
210 | 257 | |
211 | 258 | |
212 |
- |