Skip to content

Commit 297bf8d

Browse files
committed
Test options in rest interface against live server; rest doc update
Add OPTIONS verb routing for /rest and /rest/data Document that there must not be a content-type header for OPTIONS or GET. Set TRACKER_WEB option in config.in to match where the server is running. Add testing for OPTIONS verb against all rest endpoint types.
1 parent de65a3e commit 297bf8d

File tree

3 files changed

+163
-3
lines changed

3 files changed

+163
-3
lines changed

doc/rest.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,10 @@ described above.
190190
Input Formats
191191
=============
192192

193-
Content-Type is allowed to be ``application/json`` or
193+
For a GET or OPTIONS request, the Content-Type header should
194+
not be sent.
195+
196+
Otherwise Content-Type is allowed to be ``application/json`` or
194197
``application/x-www-form-urlencoded``. Any other value returns error
195198
code 415.
196199

roundup/rest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,6 +1715,21 @@ def describe(self, input):
17151715

17161716
return 200, result
17171717

1718+
@Routing.route("/", 'OPTIONS')
1719+
@_data_decorator
1720+
def options_describe(self, input):
1721+
"""OPTION return the HTTP Header for the root
1722+
1723+
Returns:
1724+
int: http status code 204 (No content)
1725+
body (string): an empty string
1726+
"""
1727+
self.client.setHeader(
1728+
"Allow",
1729+
"OPTIONS, GET"
1730+
)
1731+
return 204, ""
1732+
17181733
@Routing.route("/data")
17191734
@_data_decorator
17201735
def data(self, input):
@@ -1729,6 +1744,21 @@ def data(self, input):
17291744
result[cls] = dict(link=self.base_path + '/data/' + cls)
17301745
return 200, result
17311746

1747+
@Routing.route("/data", 'OPTIONS')
1748+
@_data_decorator
1749+
def options_data(self, input):
1750+
"""OPTION return the HTTP Header for the /data element
1751+
1752+
Returns:
1753+
int: http status code 204 (No content)
1754+
body (string): an empty string
1755+
"""
1756+
self.client.setHeader(
1757+
"Allow",
1758+
"OPTIONS, GET"
1759+
)
1760+
return 204, ""
1761+
17321762
@Routing.route("/summary")
17331763
@_data_decorator
17341764
def summary(self, input):

test/test_liveserver.py

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@
1111
from .pytest_patcher import mark_class
1212
skip_requests = mark_class(pytest.mark.skip(
1313
reason='Skipping liveserver tests: requests library not available'))
14-
14+
15+
1516
@skip_requests
1617
class SimpleTest(LiveServerTestCase):
17-
port_range = (9001, 9010) # default is (8080, 8090)
18+
# have chicken and egg issue here. Need to encode the base_url
19+
# in the config file but we don't know it until after
20+
# the server is started nd has read the config.ini.
21+
# so only allow one port number
22+
port_range = (9001, 9001) # default is (8080, 8090)
1823

1924
dirname = '_test_instance'
2025
backend = 'anydbm'
@@ -33,6 +38,10 @@ def setup_class(cls):
3338
# open the database
3439
cls.db = cls.instance.open('admin')
3540

41+
# set the url the test instance will run at.
42+
cls.db.config['TRACKER_WEB'] = "http://localhost:9001/"
43+
cls.db.config.save()
44+
3645
cls.db.commit()
3746
cls.db.close()
3847

@@ -56,5 +65,123 @@ def test_start_page(self):
5665
""" simple test that verifies that the server can serve a start page.
5766
"""
5867
f = requests.get(self.url_base())
68+
self.assertEqual(f.status_code, 200)
5969
self.assertTrue(b'Roundup' in f.content)
6070
self.assertTrue(b'Creator' in f.content)
71+
72+
73+
def disable_test_http_options(self):
74+
""" options returns an unimplemented error for this case."""
75+
'''note this currently triggers an assertion failure in the
76+
python wsgi handler, so disable while investigating'''
77+
78+
# do not send content-type header for options
79+
f = requests.options(self.url_base() + '/',
80+
headers = {'content-type': ""})
81+
# options is not implemented for the non-rest interface.
82+
self.assertEqual(f.status_code, 501)
83+
84+
def test_rest_endpoint_root_options(self):
85+
# use basic auth for rest endpoint
86+
f = requests.options(self.url_base() + '/rest',
87+
auth=('admin', 'sekrit'),
88+
headers = {'content-type': ""})
89+
print(f.status_code)
90+
print(f.headers)
91+
92+
self.assertEqual(f.status_code, 204)
93+
expected = { 'Content-Type': 'application/json',
94+
'Access-Control-Allow-Origin': '*',
95+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override',
96+
'Allow': 'OPTIONS, GET',
97+
'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH',
98+
}
99+
100+
# use dict comprehension to remove fields like date,
101+
# content-length etc. from f.headers.
102+
self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
103+
104+
def test_rest_endpoint_data_options(self):
105+
# use basic auth for rest endpoint
106+
f = requests.options(self.url_base() + '/rest/data',
107+
auth=('admin', 'sekrit'),
108+
headers = {'content-type': ""}
109+
)
110+
print(f.status_code)
111+
print(f.headers)
112+
113+
self.assertEqual(f.status_code, 204)
114+
expected = { 'Content-Type': 'application/json',
115+
'Access-Control-Allow-Origin': '*',
116+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override',
117+
'Allow': 'OPTIONS, GET',
118+
'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH',
119+
}
120+
121+
# use dict comprehension to remove fields like date,
122+
# content-length etc. from f.headers.
123+
self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
124+
125+
def test_rest_endpoint_collection_options(self):
126+
# use basic auth for rest endpoint
127+
f = requests.options(self.url_base() + '/rest/data/user',
128+
auth=('admin', 'sekrit'),
129+
headers = {'content-type': ""})
130+
print(f.status_code)
131+
print(f.headers)
132+
133+
self.assertEqual(f.status_code, 204)
134+
expected = { 'Content-Type': 'application/json',
135+
'Access-Control-Allow-Origin': '*',
136+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override',
137+
'Allow': 'OPTIONS, GET, POST',
138+
'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH',
139+
}
140+
141+
# use dict comprehension to remove fields like date,
142+
# content-length etc. from f.headers.
143+
self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
144+
145+
146+
def test_rest_endpoint_item_options(self):
147+
148+
f = requests.options(self.url_base() + '/rest/data/user/1',
149+
auth=('admin', 'sekrit'),
150+
headers = {'content-type': ""})
151+
print(f.status_code)
152+
print(f.headers)
153+
154+
self.assertEqual(f.status_code, 204)
155+
expected = { 'Content-Type': 'application/json',
156+
'Access-Control-Allow-Origin': '*',
157+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override',
158+
'Allow': 'OPTIONS, GET, PUT, DELETE, PATCH',
159+
'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH',
160+
}
161+
162+
# use dict comprehension to remove fields like date,
163+
# content-length etc. from f.headers.
164+
self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
165+
166+
def test_rest_endpoint_element_options(self):
167+
# use basic auth for rest endpoint
168+
f = requests.options(self.url_base() + '/rest/data/user/1/username',
169+
auth=('admin', 'sekrit'),
170+
headers = {'content-type': ""})
171+
print(f.status_code)
172+
print(f.headers)
173+
174+
self.assertEqual(f.status_code, 204)
175+
expected = { 'Content-Type': 'application/json',
176+
'Access-Control-Allow-Origin': '*',
177+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override',
178+
'Allow': 'OPTIONS, GET, PUT, DELETE, PATCH',
179+
'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH',
180+
}
181+
182+
# use dict comprehension to remove fields like date,
183+
# content-length etc. from f.headers.
184+
self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
185+
186+
187+

0 commit comments

Comments
 (0)