Skip to content

Commit 19fc7fc

Browse files
committed
Put together first pass of a self contained core service API client.
1 parent 0504535 commit 19fc7fc

File tree

3 files changed

+226
-6
lines changed

3 files changed

+226
-6
lines changed

mig/lib/coreapi/__init__.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import codecs
2+
import json
3+
import werkzeug.exceptions as httpexceptions
4+
5+
from tests.support._env import PY2
6+
7+
if PY2:
8+
from urllib2 import HTTPError, Request, urlopen
9+
from urllib import urlencode
10+
else:
11+
from urllib.error import HTTPError
12+
from urllib.parse import urlencode
13+
from urllib.request import urlopen, Request
14+
15+
from mig.lib.coresvc.payloads import PAYLOAD_POST_USER
16+
17+
18+
httpexceptions_by_code = {
19+
exc.code: exc for exc in httpexceptions.__dict__.values() if hasattr(exc, 'code')}
20+
21+
22+
def attempt_to_decode_response_data(data, response_encoding=None):
23+
if data is None:
24+
return None
25+
elif response_encoding == 'textual':
26+
data = codecs.decode(data, 'utf8')
27+
28+
try:
29+
return json.loads(data)
30+
except Exception as e:
31+
return data
32+
elif response_encoding == 'binary':
33+
return data
34+
else:
35+
raise AssertionError(
36+
'issue_POST: unknown response_encoding "%s"' % (response_encoding,))
37+
38+
39+
def http_error_from_status_code(http_status_code, description=None):
40+
return httpexceptions_by_code[http_status_code](description)
41+
42+
43+
class CoreApiClient:
44+
def __init__(self, base_url):
45+
self._base_url = base_url
46+
47+
def _issue_GET(self, request_path, query_dict=None, response_encoding='textual'):
48+
request_url = ''.join((self._base_url, request_path))
49+
50+
if query_dict is not None:
51+
query_string = urlencode(query_dict)
52+
request_url = ''.join((request_url, '?', query_string))
53+
54+
status = 0
55+
data = None
56+
57+
try:
58+
response = urlopen(request_url, None, timeout=2000)
59+
60+
status = response.getcode()
61+
data = response.read()
62+
except HTTPError as httpexc:
63+
status = httpexc.code
64+
data = None
65+
66+
content = attempt_to_decode_response_data(data, response_encoding)
67+
return (status, content)
68+
69+
def _issue_POST(self, request_path, request_data=None, request_json=None, response_encoding='textual'):
70+
request_url = ''.join((self._base_url, request_path))
71+
72+
if request_data and request_json:
73+
raise ValueError(
74+
"only one of data or json request data may be specified")
75+
76+
status = 0
77+
data = None
78+
79+
try:
80+
if request_json is not None:
81+
request_data = codecs.encode(json.dumps(request_json), 'utf8')
82+
request_headers = {
83+
'Content-Type': 'application/json'
84+
}
85+
request = Request(request_url, request_data,
86+
headers=request_headers)
87+
elif request_data is not None:
88+
request = Request(request_url, request_data)
89+
else:
90+
request = Request(request_url)
91+
92+
response = urlopen(request, timeout=2000)
93+
94+
status = response.getcode()
95+
data = response.read()
96+
except HTTPError as httpexc:
97+
status = httpexc.code
98+
data = httpexc.fp.read()
99+
100+
content = attempt_to_decode_response_data(data, response_encoding)
101+
return (status, content)
102+
103+
def createUser(self, user_dict):
104+
payload = PAYLOAD_POST_USER.ensure(user_dict)
105+
106+
status, output = self._issue_POST('/user', request_json=dict(payload))
107+
if status != 201:
108+
description = output if isinstance(output, str) else None
109+
raise http_error_from_status_code(status, description)
110+
return output

tests/support/serversupp.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,16 @@ class ServerWithinThreadExecutor:
4141

4242
def __init__(self, ServerClass, *args, **kwargs):
4343
self._serverclass = ServerClass
44-
self._serverclass_on_instance = kwargs.pop('on_instance')
44+
self._serverclass_on_instance = kwargs.pop('on_instance', None)
4545
self._arguments = (args, kwargs)
4646
self._started = ThreadEvent()
4747
self._thread = None
4848
self._wrapped = None
4949

50+
def __getattr__(self, attr):
51+
assert self._wrapped, "wrapped instance was not created"
52+
return getattr(self._wrapped, attr)
53+
5054
def run(self):
5155
"""Mimic the same method from the standard thread API"""
5256
server_args, server_kwargs = self._arguments
@@ -76,14 +80,16 @@ def start_wait_until_ready(self):
7680
def stop(self):
7781
"""Mimic the same method from the standard thread API"""
7882
self.stop_server()
79-
self._wrapped = None
80-
self._thread.join()
81-
self._thread = None
83+
if self._thread:
84+
self._thread.join()
85+
self._thread = None
8286

8387
def stop_server(self):
8488
"""Stop server thread"""
85-
self._wrapped.shutdown()
86-
self._wrapped.server_close()
89+
if self._wrapped:
90+
self._wrapped.shutdown()
91+
self._wrapped.server_close()
92+
self._wrapped = None
8793

8894

8995
def make_wrapped_server(ServerClass, *args, **kwargs):

tests/test_mig_lib_coreapi.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import codecs
2+
import json
3+
from http.server import HTTPServer, BaseHTTPRequestHandler
4+
5+
from tests.support import MigTestCase, testmain
6+
from tests.support.serversupp import make_wrapped_server
7+
8+
from mig.lib.coreapi import CoreApiClient
9+
10+
11+
class TestRequestHandler(BaseHTTPRequestHandler):
12+
def do_POST(self):
13+
test_server = self.server
14+
15+
if test_server._programmed_response:
16+
status, content = test_server._programmed_response
17+
elif test_server._programmed_error:
18+
status, content = test_server._programmed_error
19+
20+
self.send_response(status)
21+
self.end_headers()
22+
self.wfile.write(content)
23+
24+
25+
class TestHTTPServer(HTTPServer):
26+
def __init__(self, addr, **kwargs):
27+
self._programmed_error = None
28+
self._programmed_response = None
29+
self._on_start = kwargs.pop('on_start', lambda _: None)
30+
31+
HTTPServer.__init__(self, addr, TestRequestHandler, **kwargs)
32+
33+
def clear_programmed(self):
34+
self._programmed_error = None
35+
36+
def set_programmed_error(self, status, content):
37+
assert self._programmed_response is None
38+
assert isinstance(content, bytes)
39+
self._programmed_error = (status, content)
40+
41+
def set_programmed_response(self, status, content):
42+
assert self._programmed_error is None
43+
assert isinstance(content, bytes)
44+
self._programmed_response = (status, content)
45+
46+
def set_programmed_json_response(self, status, content):
47+
self.set_programmed_response(status, codecs.encode(json.dumps(content), 'utf8'))
48+
49+
def server_activate(self):
50+
HTTPServer.server_activate(self)
51+
self._on_start(self)
52+
53+
54+
class TestMigLibCoreapi(MigTestCase):
55+
def before_each(self):
56+
self.server_addr = ('localhost', 4567)
57+
self.server = make_wrapped_server(TestHTTPServer, self.server_addr)
58+
59+
def after_each(self):
60+
server = getattr(self, 'server', None)
61+
setattr(self, 'server', None)
62+
if server:
63+
server.stop()
64+
65+
def test_raises_in_the_absence_of_success(self):
66+
self.server.start_wait_until_ready()
67+
self.server.set_programmed_error(418, b'tea; earl grey; hot')
68+
instance = CoreApiClient("http://%s:%s/" % self.server_addr)
69+
70+
with self.assertRaises(Exception):
71+
instance.createUser({
72+
'full_name': "Test User",
73+
'organization': "Test Org",
74+
'state': "NA",
75+
'country': "DK",
76+
'email': "[email protected]",
77+
'comment': "This is the create comment",
78+
'password': "password",
79+
})
80+
81+
def test_returs_a_user_object(self):
82+
test_content = {
83+
'foo': 1,
84+
'bar': True
85+
}
86+
self.server.start_wait_until_ready()
87+
self.server.set_programmed_json_response(201, test_content)
88+
instance = CoreApiClient("http://%s:%s/" % self.server_addr)
89+
90+
content = instance.createUser({
91+
'full_name': "Test User",
92+
'organization': "Test Org",
93+
'state': "NA",
94+
'country': "DK",
95+
'email': "[email protected]",
96+
'comment': "This is the create comment",
97+
'password': "password",
98+
})
99+
100+
self.assertIsInstance(content, dict)
101+
self.assertEqual(content, test_content)
102+
103+
if __name__ == '__main__':
104+
testmain()

0 commit comments

Comments
 (0)