Merge lp:~allenap/maas/query-strings-and-request-bodies into lp:maas/trunk

Proposed by Gavin Panella on 2012-10-02
Status: Merged
Approved by: Gavin Panella on 2012-10-02
Approved revision: 1131
Merged at revision: 1132
Proposed branch: lp:~allenap/maas/query-strings-and-request-bodies
Merge into: lp:maas/trunk
Diff against target: 280 lines (+146/-71)
2 files modified
src/maascli/api.py (+18/-22)
src/maascli/tests/test_api.py (+128/-49)
To merge this branch: bzr merge lp:~allenap/maas/query-strings-and-request-bodies
Reviewer Review Type Date Requested Status
Raphaël Badin (community) 2012-10-02 Approve on 2012-10-02
Review via email: mp+127479@code.launchpad.net

Commit Message

For the CLI, only encode request data in the query string for GET requests.

Previously, request data was being encoded in the query string for PUT and DELETE requests too.

To post a comment you must log in.
Raphaël Badin (rvb) wrote :

Looks good. ("from itertools import chain", you like that method don't you ;) )

review: Approve
Gavin Panella (allenap) wrote :

> Looks good. ("from itertools import chain", you like that method don't you ;)

Yeah, it's teh awesome :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maascli/api.py'
2--- src/maascli/api.py 2012-09-27 01:39:52 +0000
3+++ src/maascli/api.py 2012-10-02 13:04:33 +0000
4@@ -16,6 +16,7 @@
5
6 from getpass import getpass
7 import httplib
8+from itertools import chain
9 import json
10 import sys
11 from urlparse import (
12@@ -193,7 +194,6 @@
13
14 uri = property(lambda self: self.handler["uri"])
15 method = property(lambda self: self.action["method"])
16- is_restful = property(lambda self: self.action["restful"])
17 credentials = property(lambda self: self.profile["credentials"])
18 op = property(lambda self: self.action["op"])
19
20@@ -210,13 +210,9 @@
21 # <https://github.com/uri-templates/uritemplate-py> here?
22 uri = self.uri.format(**vars(options))
23
24- # Add the operation to the data set.
25- if self.op is not None:
26- options.data.append(("op", self.op))
27-
28 # Bundle things up ready to throw over the wire.
29 uri, body, headers = self.prepare_payload(
30- self.method, self.is_restful, uri, options.data)
31+ self.op, self.method, uri, options.data)
32
33 # Sign request if credentials have been provided.
34 if self.credentials is not None:
35@@ -247,31 +243,31 @@
36 "%r is not a name=value pair" % string)
37
38 @staticmethod
39- def prepare_payload(method, is_restful, uri, data):
40+ def prepare_payload(op, method, uri, data):
41 """Return the URI (modified perhaps) and body and headers.
42
43+ - For GET requests, encode parameters in the query string.
44+
45+ - Otherwise always encode parameters in the request body.
46+
47+ - Except op; this can always go in the query string.
48+
49 :param method: The HTTP method.
50- :param is_restful: Is this a ReSTful operation?
51 :param uri: The URI of the action.
52 :param data: A dict or iterable of name=value pairs to pack into the
53 body or headers, depending on the type of request.
54 """
55- if method == "POST" and not is_restful:
56- # Encode the data as multipart for non-ReSTful POST requests; all
57- # others should use query parameters. TODO: encode_multipart_data
58- # insists on a dict for the data, which prevents specifying
59- # multiple values for a field, like mac_addresses. This needs to
60- # be fixed.
61- body, headers = encode_multipart_data(data, {})
62- # TODO: make encode_multipart_data work with arbitrarily encoded
63- # strings; atm, it blows up when encountering a non-ASCII string.
64- else:
65- # TODO: deal with state information, i.e. where to stuff CRUD
66- # data, content types, etc.
67+ if method == "GET":
68+ query = data if op is None else chain([("op", op)], data)
69 body, headers = None, {}
70- # TODO: smarter merging of data with query.
71- uri = urlparse(uri)._replace(query=urlencode(data)).geturl()
72+ else:
73+ query = [] if op is None else [("op", op)]
74+ if data:
75+ body, headers = encode_multipart_data(data)
76+ else:
77+ body, headers = None, {}
78
79+ uri = urlparse(uri)._replace(query=urlencode(query)).geturl()
80 return uri, body, headers
81
82 @staticmethod
83
84=== modified file 'src/maascli/tests/test_api.py'
85--- src/maascli/tests/test_api.py 2012-09-27 03:02:58 +0000
86+++ src/maascli/tests/test_api.py 2012-10-02 13:04:33 +0000
87@@ -27,7 +27,6 @@
88 from mock import sentinel
89 from testtools.matchers import (
90 Equals,
91- Is,
92 IsInstance,
93 MatchesAll,
94 MatchesListwise,
95@@ -154,57 +153,137 @@
96 api.Action.name_value_pair(" foo = bar "))
97
98
99-class TestActionReSTful(TestCase):
100- """Tests for ReSTful operations in `maascli.api.Action`."""
101-
102- scenarios = (
103- ("create", dict(method="POST")),
104- ("read", dict(method="GET")),
105- ("update", dict(method="PUT")),
106- ("delete", dict(method="DELETE")),
107- )
108-
109- def test_prepare_payload_without_data(self):
110- # prepare_payload() is almost a no-op for ReSTful methods that don't
111- # specify any extra data.
112- uri_base = "http://example.com/MAAS/api/1.0/"
113- payload = api.Action.prepare_payload(
114- method=self.method, is_restful=True, uri=uri_base, data=[])
115- expected = (
116- Equals(uri_base), # uri
117- Is(None), # body
118- Equals({}), # headers
119- )
120- self.assertThat(payload, MatchesListwise(expected))
121-
122- def test_prepare_payload_with_data(self):
123- # Given data is always encoded as query parameters.
124- uri_base = "http://example.com/MAAS/api/1.0/"
125- payload = api.Action.prepare_payload(
126- method=self.method, is_restful=True, uri=uri_base,
127- data=[("foo", "bar"), ("foo", "baz")])
128- expected = (
129- Equals(uri_base + "?foo=bar&foo=baz"), # uri
130- Is(None), # body
131- Equals({}), # headers
132- )
133- self.assertThat(payload, MatchesListwise(expected))
134-
135-
136-class TestActionOperations(TestCase):
137- """Tests for non-ReSTful operations in `maascli.api.Action`."""
138-
139- def test_prepare_payload_POST_non_restful(self):
140- # Non-ReSTful POSTs encode the given data using encode_multipart_data.
141+class TestPayloadPreparation(TestCase):
142+ """Tests for `maascli.api.Action.prepare_payload`."""
143+
144+ uri_base = "http://example.com/MAAS/api/1.0/"
145+
146+ # Scenarios for ReSTful operations; i.e. without an "op" parameter.
147+ scenarios_without_op = (
148+ # Without data, all requests have an empty request body and no extra
149+ # headers.
150+ ("create",
151+ {"method": "POST", "data": [],
152+ "expected_uri": uri_base,
153+ "expected_body": None,
154+ "expected_headers": {}}),
155+ ("read",
156+ {"method": "GET", "data": [],
157+ "expected_uri": uri_base,
158+ "expected_body": None,
159+ "expected_headers": {}}),
160+ ("update",
161+ {"method": "PUT", "data": [],
162+ "expected_uri": uri_base,
163+ "expected_body": None,
164+ "expected_headers": {}}),
165+ ("delete",
166+ {"method": "DELETE", "data": [],
167+ "expected_uri": uri_base,
168+ "expected_body": None,
169+ "expected_headers": {}}),
170+ # With data, PUT, POST, and DELETE requests have their body and extra
171+ # headers prepared by encode_multipart_data. For GET requests, the
172+ # data is encoded into the query string, and both the request body and
173+ # extra headers are empty.
174+ ("create-with-data",
175+ {"method": "POST", "data": [("foo", "bar"), ("foo", "baz")],
176+ "expected_uri": uri_base,
177+ "expected_body": sentinel.body,
178+ "expected_headers": sentinel.headers}),
179+ ("read-with-data",
180+ {"method": "GET", "data": [("foo", "bar"), ("foo", "baz")],
181+ "expected_uri": uri_base + "?foo=bar&foo=baz",
182+ "expected_body": None,
183+ "expected_headers": {}}),
184+ ("update-with-data",
185+ {"method": "PUT", "data": [("foo", "bar"), ("foo", "baz")],
186+ "expected_uri": uri_base,
187+ "expected_body": sentinel.body,
188+ "expected_headers": sentinel.headers}),
189+ ("delete-with-data",
190+ {"method": "DELETE", "data": [("foo", "bar"), ("foo", "baz")],
191+ "expected_uri": uri_base,
192+ "expected_body": sentinel.body,
193+ "expected_headers": sentinel.headers}),
194+ )
195+
196+ # Scenarios for non-ReSTful operations; i.e. with an "op" parameter.
197+ scenarios_with_op = (
198+ # Without data, all requests have an empty request body and no extra
199+ # headers. The operation is encoded into the query string.
200+ ("create",
201+ {"method": "POST", "data": [],
202+ "expected_uri": uri_base + "?op=something",
203+ "expected_body": None,
204+ "expected_headers": {}}),
205+ ("read",
206+ {"method": "GET", "data": [],
207+ "expected_uri": uri_base + "?op=something",
208+ "expected_body": None,
209+ "expected_headers": {}}),
210+ ("update",
211+ {"method": "PUT", "data": [],
212+ "expected_uri": uri_base + "?op=something",
213+ "expected_body": None,
214+ "expected_headers": {}}),
215+ ("delete",
216+ {"method": "DELETE", "data": [],
217+ "expected_uri": uri_base + "?op=something",
218+ "expected_body": None,
219+ "expected_headers": {}}),
220+ # With data, PUT, POST, and DELETE requests have their body and extra
221+ # headers prepared by encode_multipart_data. For GET requests, the
222+ # data is encoded into the query string, and both the request body and
223+ # extra headers are empty. The operation is encoded into the query
224+ # string.
225+ ("create-with-data",
226+ {"method": "POST", "data": [("foo", "bar"), ("foo", "baz")],
227+ "expected_uri": uri_base + "?op=something",
228+ "expected_body": sentinel.body,
229+ "expected_headers": sentinel.headers}),
230+ ("read-with-data",
231+ {"method": "GET", "data": [("foo", "bar"), ("foo", "baz")],
232+ "expected_uri": uri_base + "?op=something&foo=bar&foo=baz",
233+ "expected_body": None,
234+ "expected_headers": {}}),
235+ ("update-with-data",
236+ {"method": "PUT", "data": [("foo", "bar"), ("foo", "baz")],
237+ "expected_uri": uri_base + "?op=something",
238+ "expected_body": sentinel.body,
239+ "expected_headers": sentinel.headers}),
240+ ("delete-with-data",
241+ {"method": "DELETE", "data": [("foo", "bar"), ("foo", "baz")],
242+ "expected_uri": uri_base + "?op=something",
243+ "expected_body": sentinel.body,
244+ "expected_headers": sentinel.headers}),
245+ )
246+
247+ scenarios_without_op = tuple(
248+ ("%s-without-op" % name, dict(scenario, op=None))
249+ for name, scenario in scenarios_without_op)
250+
251+ scenarios_with_op = tuple(
252+ ("%s-with-op" % name, dict(scenario, op="something"))
253+ for name, scenario in scenarios_with_op)
254+
255+ scenarios = scenarios_without_op + scenarios_with_op
256+
257+ def test_prepare_payload(self):
258+ # Patch encode_multipart_data to match the scenarios.
259 encode_multipart_data = self.patch(api, "encode_multipart_data")
260 encode_multipart_data.return_value = sentinel.body, sentinel.headers
261- uri_base = "http://example.com/MAAS/api/1.0/"
262+ # The payload returned is a 3-tuple of (uri, body, headers).
263 payload = api.Action.prepare_payload(
264- method="POST", is_restful=False, uri=uri_base,
265- data=[("foo", "bar"), ("foo", "baz")])
266+ op=self.op, method=self.method,
267+ uri=self.uri_base, data=self.data)
268 expected = (
269- Equals(uri_base), # uri
270- Is(sentinel.body), # body
271- Is(sentinel.headers), # headers
272+ Equals(self.expected_uri),
273+ Equals(self.expected_body),
274+ Equals(self.expected_headers),
275 )
276 self.assertThat(payload, MatchesListwise(expected))
277+ # encode_multipart_data, when called, is passed the data
278+ # unadulterated.
279+ if self.expected_body is sentinel.body:
280+ api.encode_multipart_data.assert_called_once_with(self.data)