Merge lp:~allenap/maas/has-status-code-matcher into lp:~maas-committers/maas/trunk
- has-status-code-matcher
- Merge into trunk
Proposed by
Gavin Panella
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | no longer in the source branch. |
Merged at revision: | 5311 |
Proposed branch: | lp:~allenap/maas/has-status-code-matcher |
Merge into: | lp:~maas-committers/maas/trunk |
Prerequisite: | lp:~allenap/maas/make-gecos-field |
Diff against target: |
382 lines (+81/-47) 6 files modified
src/maasserver/api/tests/test_api.py (+3/-2) src/maasserver/api/tests/test_doc.py (+2/-1) src/maasserver/api/tests/test_machines.py (+24/-23) src/maasserver/api/tests/test_node.py (+3/-1) src/maasserver/testing/api.py (+0/-20) src/maasserver/testing/matchers.py (+49/-0) |
To merge this branch: | bzr merge lp:~allenap/maas/has-status-code-matcher |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mike Pontillo (community) | Approve | ||
Review via email: mp+304503@code.launchpad.net |
Commit message
New matcher HasStatusCode.
This matches a Django HttpResponse's status code, and provides a full dump of the response on mismatch.
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/api/tests/test_api.py' |
2 | --- src/maasserver/api/tests/test_api.py 2016-08-09 21:59:28 +0000 |
3 | +++ src/maasserver/api/tests/test_api.py 2016-09-01 08:46:07 +0000 |
4 | @@ -32,6 +32,7 @@ |
5 | APITransactionTestCase, |
6 | ) |
7 | from maasserver.testing.factory import factory |
8 | +from maasserver.testing.matchers import HasStatusCode |
9 | from maasserver.testing.testcase import MAASServerTestCase |
10 | from maasserver.testing.testclient import MAASSensibleOAuthClient |
11 | from maasserver.utils.converters import json_load_bytes |
12 | @@ -325,14 +326,14 @@ |
13 | |
14 | def test_get_api_version(self): |
15 | response = self.client.get(reverse('api_version')) |
16 | - self.assertResponseCode(http.client.OK, response) |
17 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
18 | self.assertIn('text/plain', response['Content-Type']) |
19 | self.assertEqual(b'2.0', response.content) |
20 | |
21 | def test_old_api_request(self): |
22 | old_api_url = reverse('api_v1_error') + "maas/" + factory.make_string() |
23 | response = self.client.get(old_api_url) |
24 | - self.assertResponseCode(http.client.GONE, response) |
25 | + self.assertThat(response, HasStatusCode(http.client.GONE)) |
26 | self.assertIn('text/plain', response['Content-Type']) |
27 | self.assertEqual( |
28 | b'The 1.0 API is no longer available. Please use API version 2.0.', |
29 | |
30 | === modified file 'src/maasserver/api/tests/test_doc.py' |
31 | --- src/maasserver/api/tests/test_doc.py 2016-05-24 14:43:27 +0000 |
32 | +++ src/maasserver/api/tests/test_doc.py 2016-09-01 08:46:07 +0000 |
33 | @@ -40,6 +40,7 @@ |
34 | from maasserver.testing.api import APITestCase |
35 | from maasserver.testing.config import RegionConfigurationFixture |
36 | from maasserver.testing.factory import factory |
37 | +from maasserver.testing.matchers import HasStatusCode |
38 | from maastesting.matchers import ( |
39 | IsCallable, |
40 | MockCalledOnceWith, |
41 | @@ -138,7 +139,7 @@ |
42 | def test_api_doc_accessibility(self): |
43 | self.patch(sys, "stderr", StringIO()) |
44 | response = self.client.get(reverse('api-doc')) |
45 | - self.assertResponseCode(http.client.OK, response) |
46 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
47 | # No error or warning are emitted by docutils. |
48 | self.assertEqual("", sys.stderr.getvalue()) |
49 | |
50 | |
51 | === modified file 'src/maasserver/api/tests/test_machines.py' |
52 | --- src/maasserver/api/tests/test_machines.py 2016-08-16 10:36:47 +0000 |
53 | +++ src/maasserver/api/tests/test_machines.py 2016-09-01 08:46:07 +0000 |
54 | @@ -44,6 +44,7 @@ |
55 | RunningEventLoopFixture, |
56 | ) |
57 | from maasserver.testing.factory import factory |
58 | +from maasserver.testing.matchers import HasStatusCode |
59 | from maasserver.testing.testclient import MAASSensibleOAuthClient |
60 | from maasserver.utils import ignore_unused |
61 | from maasserver.utils.orm import reload_object |
62 | @@ -711,7 +712,7 @@ |
63 | 'op': 'allocate', |
64 | 'cpu_count': 2, |
65 | }) |
66 | - self.assertResponseCode(http.client.OK, response) |
67 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
68 | response_json = json.loads( |
69 | response.content.decode(settings.DEFAULT_CHARSET)) |
70 | self.assertEqual(machine.system_id, response_json['system_id']) |
71 | @@ -724,7 +725,7 @@ |
72 | 'op': 'allocate', |
73 | 'cpu_count': '1.0', |
74 | }) |
75 | - self.assertResponseCode(http.client.OK, response) |
76 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
77 | response_json = json.loads( |
78 | response.content.decode(settings.DEFAULT_CHARSET)) |
79 | self.assertEqual(machine.system_id, response_json['system_id']) |
80 | @@ -737,7 +738,7 @@ |
81 | 'op': 'allocate', |
82 | 'cpu_count': 'plenty', |
83 | }) |
84 | - self.assertResponseCode(http.client.BAD_REQUEST, response) |
85 | + self.assertThat(response, HasStatusCode(http.client.BAD_REQUEST)) |
86 | |
87 | def test_POST_allocate_allocates_machine_by_mem(self): |
88 | # Asking for enough memory acquires a machine with at least that. |
89 | @@ -747,7 +748,7 @@ |
90 | 'op': 'allocate', |
91 | 'mem': 1024, |
92 | }) |
93 | - self.assertResponseCode(http.client.OK, response) |
94 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
95 | response_json = json.loads( |
96 | response.content.decode(settings.DEFAULT_CHARSET)) |
97 | self.assertEqual(machine.system_id, response_json['system_id']) |
98 | @@ -760,7 +761,7 @@ |
99 | 'op': 'allocate', |
100 | 'mem': 'bags', |
101 | }) |
102 | - self.assertResponseCode(http.client.BAD_REQUEST, response) |
103 | + self.assertThat(response, HasStatusCode(http.client.BAD_REQUEST)) |
104 | |
105 | def test_POST_allocate_allocates_machine_by_tags(self): |
106 | machine = factory.make_Node( |
107 | @@ -772,7 +773,7 @@ |
108 | 'op': 'allocate', |
109 | 'tags': ['fast', 'stable'], |
110 | }) |
111 | - self.assertResponseCode(http.client.OK, response) |
112 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
113 | response_json = json.loads( |
114 | response.content.decode(settings.DEFAULT_CHARSET)) |
115 | self.assertItemsEqual(machine_tag_names, response_json['tag_names']) |
116 | @@ -791,7 +792,7 @@ |
117 | 'op': 'allocate', |
118 | 'not_tags': ['cute'] |
119 | }) |
120 | - self.assertResponseCode(http.client.OK, response) |
121 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
122 | response_json = json.loads( |
123 | response.content.decode(settings.DEFAULT_CHARSET)) |
124 | self.assertEqual( |
125 | @@ -810,7 +811,7 @@ |
126 | 'op': 'allocate', |
127 | 'zone': zone.name, |
128 | }) |
129 | - self.assertResponseCode(http.client.OK, response) |
130 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
131 | response_json = json.loads( |
132 | response.content.decode(settings.DEFAULT_CHARSET)) |
133 | self.assertEqual(machine.system_id, response_json['system_id']) |
134 | @@ -823,7 +824,7 @@ |
135 | 'op': 'allocate', |
136 | 'zone': zone.name, |
137 | }) |
138 | - self.assertResponseCode(http.client.CONFLICT, response) |
139 | + self.assertThat(response, HasStatusCode(http.client.CONFLICT)) |
140 | |
141 | def test_POST_allocate_rejects_unknown_zone(self): |
142 | response = self.client.post(reverse('machines_handler'), { |
143 | @@ -842,7 +843,7 @@ |
144 | 'op': 'allocate', |
145 | 'tags': 'fast, stable', |
146 | }) |
147 | - self.assertResponseCode(http.client.OK, response) |
148 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
149 | response_json = json.loads( |
150 | response.content.decode(settings.DEFAULT_CHARSET)) |
151 | self.assertItemsEqual(machine_tag_names, response_json['tag_names']) |
152 | @@ -857,7 +858,7 @@ |
153 | 'op': 'allocate', |
154 | 'tags': 'fast stable', |
155 | }) |
156 | - self.assertResponseCode(http.client.OK, response) |
157 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
158 | response_json = json.loads( |
159 | response.content.decode(settings.DEFAULT_CHARSET)) |
160 | self.assertItemsEqual(machine_tag_names, response_json['tag_names']) |
161 | @@ -872,7 +873,7 @@ |
162 | 'op': 'allocate', |
163 | 'tags': 'fast, stable cute', |
164 | }) |
165 | - self.assertResponseCode(http.client.OK, response) |
166 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
167 | response_json = json.loads( |
168 | response.content.decode(settings.DEFAULT_CHARSET)) |
169 | self.assertItemsEqual(machine_tag_names, response_json['tag_names']) |
170 | @@ -887,7 +888,7 @@ |
171 | 'op': 'allocate', |
172 | 'tags': ['fast, stable', 'cute'], |
173 | }) |
174 | - self.assertResponseCode(http.client.OK, response) |
175 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
176 | response_json = json.loads( |
177 | response.content.decode(settings.DEFAULT_CHARSET)) |
178 | self.assertItemsEqual(machine_tag_names, response_json['tag_names']) |
179 | @@ -905,7 +906,7 @@ |
180 | 'op': 'allocate', |
181 | 'storage': 'needed:10(ssd)', |
182 | }) |
183 | - self.assertResponseCode(http.client.OK, response) |
184 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
185 | response_json = json.loads( |
186 | response.content.decode(settings.DEFAULT_CHARSET)) |
187 | device_id = response_json['physicalblockdevice_set'][0]['id'] |
188 | @@ -929,7 +930,7 @@ |
189 | 'storage': 'needed:10(ssd)', |
190 | 'verbose': 'true', |
191 | }) |
192 | - self.assertResponseCode(http.client.OK, response) |
193 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
194 | response_json = json.loads( |
195 | response.content.decode(settings.DEFAULT_CHARSET)) |
196 | device_id = response_json['physicalblockdevice_set'][0]['id'] |
197 | @@ -953,7 +954,7 @@ |
198 | 'op': 'allocate', |
199 | 'interfaces': 'needed:fabric=ubuntu', |
200 | }) |
201 | - self.assertResponseCode(http.client.OK, response) |
202 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
203 | response_json = json.loads( |
204 | response.content.decode(settings.DEFAULT_CHARSET)) |
205 | self.expectThat( |
206 | @@ -981,7 +982,7 @@ |
207 | 'verbose': 'true', |
208 | 'dry_run': 'true', |
209 | }) |
210 | - self.assertResponseCode(http.client.OK, response) |
211 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
212 | response_json = json.loads( |
213 | response.content.decode(settings.DEFAULT_CHARSET)) |
214 | self.expectThat( |
215 | @@ -1015,7 +1016,7 @@ |
216 | 'interfaces': 'needed:fabric=ubuntu', |
217 | 'verbose': 'true', |
218 | }) |
219 | - self.assertResponseCode(http.client.OK, response) |
220 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
221 | response_json = json.loads( |
222 | response.content.decode(settings.DEFAULT_CHARSET)) |
223 | constraints = response_json['constraints_by_type'] |
224 | @@ -1044,7 +1045,7 @@ |
225 | 'op': 'allocate', |
226 | 'tags': 'fast, cheap', |
227 | }) |
228 | - self.assertResponseCode(http.client.CONFLICT, response) |
229 | + self.assertThat(response, HasStatusCode(http.client.CONFLICT)) |
230 | |
231 | def test_POST_allocate_fails_with_unknown_tags(self): |
232 | # Asking for a tag that does not exist gives a specific error. |
233 | @@ -1082,7 +1083,7 @@ |
234 | 'subnets': [subnets[pick].name], |
235 | }) |
236 | |
237 | - self.assertResponseCode(http.client.OK, response) |
238 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
239 | response_json = json.loads( |
240 | response.content.decode(settings.DEFAULT_CHARSET)) |
241 | self.assertEqual( |
242 | @@ -1104,7 +1105,7 @@ |
243 | 'not_subnets': [subnet.name for subnet in subnets], |
244 | }) |
245 | |
246 | - self.assertResponseCode(http.client.OK, response) |
247 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
248 | response_json = json.loads( |
249 | response.content.decode(settings.DEFAULT_CHARSET)) |
250 | self.assertEqual(right_machine.system_id, response_json['system_id']) |
251 | @@ -1143,7 +1144,7 @@ |
252 | status=available_status, owner=None, with_boot_disk=True) |
253 | response = self.client.post( |
254 | reverse('machines_handler'), {'op': 'allocate'}) |
255 | - self.assertResponseCode(http.client.OK, response) |
256 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
257 | machine = Machine.objects.get(system_id=machine.system_id) |
258 | oauth_key = self.client.token.key |
259 | self.assertEqual(oauth_key, machine.token.key) |
260 | @@ -2040,7 +2041,7 @@ |
261 | response = self.client.get( |
262 | self.get_machine_uri(machine), {"op": "query_power_state"}) |
263 | |
264 | - self.assertResponseCode(http.client.OK, response) |
265 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
266 | response = json.loads( |
267 | response.content.decode(settings.DEFAULT_CHARSET)) |
268 | self.assertEqual({"state": random_state}, response) |
269 | |
270 | === modified file 'src/maasserver/api/tests/test_node.py' |
271 | --- src/maasserver/api/tests/test_node.py 2016-06-17 07:16:39 +0000 |
272 | +++ src/maasserver/api/tests/test_node.py 2016-09-01 08:46:07 +0000 |
273 | @@ -25,6 +25,7 @@ |
274 | from maasserver.testing.api import APITestCase |
275 | from maasserver.testing.architecture import make_usable_architecture |
276 | from maasserver.testing.factory import factory |
277 | +from maasserver.testing.matchers import HasStatusCode |
278 | from maasserver.testing.osystems import make_usable_osystem |
279 | from maasserver.testing.testcase import MAASServerTestCase |
280 | from maasserver.testing.testclient import MAASSensibleOAuthClient |
281 | @@ -473,7 +474,8 @@ |
282 | 'stop').side_effect = PowerActionAlreadyInProgress(exc_text) |
283 | response = self.client.post( |
284 | self.get_node_uri(machine), {'op': 'power_off'}) |
285 | - self.assertResponseCode(http.client.SERVICE_UNAVAILABLE, response) |
286 | + self.assertThat(response, HasStatusCode( |
287 | + http.client.SERVICE_UNAVAILABLE)) |
288 | self.assertIn( |
289 | exc_text, response.content.decode(settings.DEFAULT_CHARSET)) |
290 | |
291 | |
292 | === modified file 'src/maasserver/testing/api.py' |
293 | --- src/maasserver/testing/api.py 2016-05-24 22:05:45 +0000 |
294 | +++ src/maasserver/testing/api.py 2016-09-01 08:46:07 +0000 |
295 | @@ -30,10 +30,6 @@ |
296 | MAASTestType, |
297 | ) |
298 | from testscenarios import multiply_scenarios |
299 | -from testtools.content import ( |
300 | - Content, |
301 | - UTF8_TEXT, |
302 | -) |
303 | |
304 | |
305 | def merge_scenarios(*scenario_lists): |
306 | @@ -181,22 +177,6 @@ |
307 | self.user.is_superuser = True |
308 | self.user.save() |
309 | |
310 | - def assertResponseCode(self, expected_code, response): |
311 | - """Assert that `response` has the HTTP status `expected_code`. |
312 | - |
313 | - :param expected_code: An integer HTTP status code. |
314 | - :param response: An instance of Django's `HttpResponse`. |
315 | - """ |
316 | - if response.status_code != expected_code: |
317 | - response_dump = response.serialize() |
318 | - if response.charset.lower() not in {"utf-8", "utf_8", "utf8"}: |
319 | - response_dump = response_dump.decode(response.charset) |
320 | - response_dump = response_dump.encode("utf-8", "replace") |
321 | - response_content = Content(UTF8_TEXT, lambda: [response_dump]) |
322 | - self.addDetail("Unexpected HTTP response", response_content) |
323 | - self.fail("Expected HTTP %s, got %s" % ( |
324 | - expected_code, response.status_code)) |
325 | - |
326 | def client_log_in(self, as_admin=False): |
327 | self.fail( |
328 | "client_log_in is broken. Use become_admin() " |
329 | |
330 | === added file 'src/maasserver/testing/matchers.py' |
331 | --- src/maasserver/testing/matchers.py 1970-01-01 00:00:00 +0000 |
332 | +++ src/maasserver/testing/matchers.py 2016-09-01 08:46:07 +0000 |
333 | @@ -0,0 +1,49 @@ |
334 | +# Copyright 2016 Canonical Ltd. This software is licensed under the |
335 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
336 | + |
337 | +"""Custom matchers for testing in the region.""" |
338 | + |
339 | +__all__ = [ |
340 | + 'HasStatusCode', |
341 | +] |
342 | + |
343 | +from testtools.content import ( |
344 | + Content, |
345 | + UTF8_TEXT, |
346 | +) |
347 | +from testtools.matchers import ( |
348 | + Matcher, |
349 | + Mismatch, |
350 | +) |
351 | + |
352 | + |
353 | +class HasStatusCode(Matcher): |
354 | + """Match if the given response has the expected HTTP status. |
355 | + |
356 | + In case of a mismatch this assumes that the response contains a textual |
357 | + body. If it's not already encoded as UTF-8, it is recoded, replacing any |
358 | + problematic characters in the process, like surrogate escapes. |
359 | + |
360 | + The response against which this matches is expected to be an instance of |
361 | + Django's `HttpResponse` though this is not explicitly tested. |
362 | + """ |
363 | + |
364 | + def __init__(self, status_code): |
365 | + super(HasStatusCode, self).__init__() |
366 | + self.status_code = status_code |
367 | + |
368 | + def match(self, response): |
369 | + if response.status_code != self.status_code: |
370 | + response_dump = response.serialize() |
371 | + if response.charset.lower() not in {"utf-8", "utf_8", "utf8"}: |
372 | + response_dump = response_dump.decode(response.charset) |
373 | + response_dump = response_dump.encode("utf-8", "replace") |
374 | + |
375 | + description = "Expected HTTP %s, got %s" % ( |
376 | + self.status_code, response.status_code) |
377 | + details = { |
378 | + "Unexpected HTTP response": Content( |
379 | + UTF8_TEXT, lambda: [response_dump]), |
380 | + } |
381 | + |
382 | + return Mismatch(description, details) |
Nice!