Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
RestResponse |
|
| 1.6428571428571428;1.643 | ||||
RestResponse$StatusMatch |
|
| 1.6428571428571428;1.643 |
1 | /** | |
2 | * Copyright (c) 2011-2017, jcabi.com | |
3 | * All rights reserved. | |
4 | * | |
5 | * Redistribution and use in source and binary forms, with or without | |
6 | * modification, are permitted provided that the following conditions | |
7 | * are met: 1) Redistributions of source code must retain the above | |
8 | * copyright notice, this list of conditions and the following | |
9 | * disclaimer. 2) Redistributions in binary form must reproduce the above | |
10 | * copyright notice, this list of conditions and the following | |
11 | * disclaimer in the documentation and/or other materials provided | |
12 | * with the distribution. 3) Neither the name of the jcabi.com nor | |
13 | * the names of its contributors may be used to endorse or promote | |
14 | * products derived from this software without specific prior written | |
15 | * permission. | |
16 | * | |
17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
18 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT | |
19 | * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND | |
20 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL | |
21 | * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, | |
22 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
24 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
25 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, | |
26 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
27 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED | |
28 | * OF THE POSSIBILITY OF SUCH DAMAGE. | |
29 | */ | |
30 | package com.jcabi.http.response; | |
31 | ||
32 | import com.jcabi.aspects.Immutable; | |
33 | import com.jcabi.http.Request; | |
34 | import com.jcabi.http.Response; | |
35 | import com.jcabi.log.Logger; | |
36 | import java.net.HttpCookie; | |
37 | import java.net.URI; | |
38 | import java.util.Collections; | |
39 | import java.util.Iterator; | |
40 | import java.util.List; | |
41 | import java.util.Map; | |
42 | import javax.ws.rs.core.Cookie; | |
43 | import javax.ws.rs.core.HttpHeaders; | |
44 | import lombok.EqualsAndHashCode; | |
45 | import org.hamcrest.CustomMatcher; | |
46 | import org.hamcrest.Matcher; | |
47 | import org.hamcrest.MatcherAssert; | |
48 | import org.hamcrest.Matchers; | |
49 | ||
50 | /** | |
51 | * REST response. | |
52 | * | |
53 | * <p>This response decorator is able to make basic assertions on | |
54 | * HTTP response and manipulate with it afterwords, for example: | |
55 | * | |
56 | * <pre> String name = new JdkRequest("http://my.example.com") | |
57 | * .fetch() | |
58 | * .as(RestResponse.class) | |
59 | * .assertStatus(200) | |
60 | * .assertBody(Matchers.containsString("hello, world!")) | |
61 | * .assertHeader("Content-Type", Matchers.hasItem("text/plain")) | |
62 | * .jump(URI.create("/users")) | |
63 | * .fetch();</pre> | |
64 | * | |
65 | * <p>Method {@link #jump(URI)} creates a new instance of class | |
66 | * {@link Request} with all cookies transferred from the current one. | |
67 | * | |
68 | * <p>The class is immutable and thread-safe. | |
69 | * | |
70 | * @author Yegor Bugayenko (yegor@tpc2.com) | |
71 | * @version $Id: 8b3634c366e075b7fd59c89cbc3839a1cbc26fb7 $ | |
72 | * @since 0.8 | |
73 | */ | |
74 | 54 | @Immutable |
75 | 0 | @EqualsAndHashCode(callSuper = true) |
76 | @SuppressWarnings("PMD.TooManyMethods") | |
77 | public final class RestResponse extends AbstractResponse { | |
78 | ||
79 | /** | |
80 | * Public ctor. | |
81 | * @param resp Response | |
82 | */ | |
83 | public RestResponse(final Response resp) { | |
84 | 131 | super(resp); |
85 | 131 | } |
86 | ||
87 | /** | |
88 | * Assert using custom matcher. | |
89 | * @param matcher The matcher to use | |
90 | * @return The same object | |
91 | */ | |
92 | public RestResponse assertThat(final Matcher<Response> matcher) { | |
93 | 0 | MatcherAssert.assertThat( |
94 | String.format("HTTP response is not valid: %s", this), | |
95 | this, | |
96 | matcher | |
97 | ); | |
98 | 0 | return this; |
99 | } | |
100 | ||
101 | /** | |
102 | * Verifies HTTP response status code against the provided absolute value, | |
103 | * and throws {@link AssertionError} in case of mismatch. | |
104 | * @param status Expected status code | |
105 | * @return The same object | |
106 | */ | |
107 | public RestResponse assertStatus(final int status) { | |
108 | 114 | final String message = String.format( |
109 | "HTTP response with status %d", status | |
110 | ); | |
111 | 114 | MatcherAssert.assertThat( |
112 | String.format( | |
113 | "HTTP response status is not equal to %d:%n%s", | |
114 | status, this | |
115 | ), | |
116 | this, | |
117 | new RestResponse.StatusMatch(message, status) | |
118 | ); | |
119 | 113 | return this; |
120 | } | |
121 | ||
122 | /** | |
123 | * Verifies HTTP response status code against the provided matcher, | |
124 | * and throws {@link AssertionError} in case of mismatch. | |
125 | * @param matcher Matcher to validate status code | |
126 | * @return This object | |
127 | */ | |
128 | public RestResponse assertStatus(final Matcher<Integer> matcher) { | |
129 | 2 | MatcherAssert.assertThat( |
130 | String.format( | |
131 | "HTTP response status is not the one expected:%n%s", | |
132 | this | |
133 | ), | |
134 | this.status(), matcher | |
135 | ); | |
136 | 2 | return this; |
137 | } | |
138 | ||
139 | /** | |
140 | * Verifies HTTP response body content against provided matcher, | |
141 | * and throws {@link AssertionError} in case of mismatch. | |
142 | * @param matcher The matcher to use | |
143 | * @return This object | |
144 | */ | |
145 | public RestResponse assertBody(final Matcher<String> matcher) { | |
146 | 53 | MatcherAssert.assertThat( |
147 | String.format( | |
148 | "HTTP response body content is not valid:%n%s", | |
149 | this | |
150 | ), | |
151 | this.body(), matcher | |
152 | ); | |
153 | 53 | return this; |
154 | } | |
155 | ||
156 | /** | |
157 | * Verifies HTTP response body content against provided matcher, | |
158 | * and throws {@link AssertionError} in case of mismatch. | |
159 | * @param matcher The matcher to use | |
160 | * @return This object | |
161 | */ | |
162 | public RestResponse assertBinary(final Matcher<byte[]> matcher) { | |
163 | 2 | MatcherAssert.assertThat( |
164 | String.format( | |
165 | "HTTP response binary content is not valid:%n%s", | |
166 | this | |
167 | ), this.binary(), | |
168 | matcher | |
169 | ); | |
170 | 2 | return this; |
171 | } | |
172 | ||
173 | /** | |
174 | * Verifies HTTP header against provided matcher, and throws | |
175 | * {@link AssertionError} in case of mismatch. | |
176 | * | |
177 | * <p>The iterator for the matcher will always be a real object an never | |
178 | * {@code NULL}, even if such a header is absent in the response. If the | |
179 | * header is absent the iterable will be empty. | |
180 | * | |
181 | * @param name Name of the header to match | |
182 | * @param matcher The matcher to use | |
183 | * @return This object | |
184 | */ | |
185 | public RestResponse assertHeader(final String name, | |
186 | final Matcher<Iterable<String>> matcher) { | |
187 | 10 | Iterable<String> values = this.headers().get(name); |
188 | 10 | if (values == null) { |
189 | 3 | values = Collections.emptyList(); |
190 | } | |
191 | 10 | MatcherAssert.assertThat( |
192 | String.format( | |
193 | "HTTP header '%s' is not valid:%n%s", | |
194 | name, this | |
195 | ), | |
196 | values, matcher | |
197 | ); | |
198 | 10 | return this; |
199 | } | |
200 | ||
201 | /** | |
202 | * Verifies HTTP header against provided matcher, and throws | |
203 | * {@link AssertionError} in case of mismatch. | |
204 | * @param name Name of the header to match | |
205 | * @param value The value to expect in one of the headers | |
206 | * @return This object | |
207 | * @since 0.9 | |
208 | */ | |
209 | public RestResponse assertHeader(final String name, final String value) { | |
210 | 2 | return this.assertHeader(name, Matchers.hasItems(value)); |
211 | } | |
212 | ||
213 | /** | |
214 | * Jump to a new location. | |
215 | * @param uri Destination to jump to | |
216 | * @return New request | |
217 | */ | |
218 | @SuppressWarnings("PMD.UseConcurrentHashMap") | |
219 | public Request jump(final URI uri) { | |
220 | 5 | Request req = this.back().uri() |
221 | .set(this.back().uri().get().resolve(uri)) | |
222 | .back(); | |
223 | 5 | final Map<String, List<String>> headers = this.headers(); |
224 | 5 | if (headers.containsKey(HttpHeaders.SET_COOKIE)) { |
225 | 2 | for (final String header : headers.get(HttpHeaders.SET_COOKIE)) { |
226 | 6 | for (final HttpCookie cookie : HttpCookie.parse(header)) { |
227 | 6 | req = req.header( |
228 | HttpHeaders.COOKIE, | |
229 | String.format( | |
230 | "%s=%s", cookie.getName(), cookie.getValue() | |
231 | ) | |
232 | ); | |
233 | 6 | } |
234 | 6 | } |
235 | } | |
236 | 5 | return req; |
237 | } | |
238 | ||
239 | /** | |
240 | * Follow LOCATION header. | |
241 | * @return New request | |
242 | */ | |
243 | public Request follow() { | |
244 | 2 | this.assertHeader( |
245 | HttpHeaders.LOCATION, | |
246 | Matchers.not(Matchers.emptyIterableOf(String.class)) | |
247 | ); | |
248 | 2 | return this.jump( |
249 | URI.create(this.headers().get(HttpHeaders.LOCATION).get(0)) | |
250 | ); | |
251 | } | |
252 | ||
253 | /** | |
254 | * Get one cookie by name. | |
255 | * @param name Cookie name | |
256 | * @return Cookie found | |
257 | */ | |
258 | @SuppressWarnings("PMD.UseConcurrentHashMap") | |
259 | public Cookie cookie(final String name) { | |
260 | 1 | final Map<String, List<String>> headers = this.headers(); |
261 | 1 | MatcherAssert.assertThat( |
262 | "cookies should be set in HTTP header", | |
263 | headers.containsKey(HttpHeaders.SET_COOKIE) | |
264 | ); | |
265 | 1 | final Iterator<String> iterator = |
266 | headers.get(HttpHeaders.SET_COOKIE).iterator(); | |
267 | 1 | final Object first = iterator.next(); |
268 | // @checkstyle MagicNumber (1 line) | |
269 | 1 | final StringBuilder buf = new StringBuilder(256); |
270 | 1 | if (first != null) { |
271 | 1 | buf.append(first); |
272 | } | |
273 | 1 | while (iterator.hasNext()) { |
274 | 0 | buf.append(','); |
275 | 0 | final Object obj = iterator.next(); |
276 | 0 | if (obj != null) { |
277 | 0 | buf.append(obj); |
278 | } | |
279 | 0 | } |
280 | 1 | final String header = buf.toString(); |
281 | 1 | Cookie cookie = null; |
282 | 1 | for (final HttpCookie candidate : HttpCookie.parse(header)) { |
283 | 1 | if (candidate.getName().equals(name)) { |
284 | 1 | cookie = RestResponse.cookie(candidate); |
285 | 1 | break; |
286 | } | |
287 | 0 | } |
288 | 1 | MatcherAssert.assertThat( |
289 | Logger.format( | |
290 | "cookie '%s' not found in Set-Cookie header: '%s'", | |
291 | name, header | |
292 | ), | |
293 | cookie, | |
294 | Matchers.notNullValue() | |
295 | ); | |
296 | 1 | assert cookie != null; |
297 | 1 | return cookie; |
298 | } | |
299 | ||
300 | /** | |
301 | * Convert HTTP cookie to a standard one. | |
302 | * @param cookie HTTP cookie | |
303 | * @return Regular one | |
304 | */ | |
305 | private static Cookie cookie(final HttpCookie cookie) { | |
306 | 1 | return new Cookie( |
307 | cookie.getName(), | |
308 | cookie.getValue(), | |
309 | cookie.getPath(), | |
310 | cookie.getDomain(), | |
311 | cookie.getVersion() | |
312 | ); | |
313 | } | |
314 | ||
315 | /** | |
316 | * Status matcher. | |
317 | */ | |
318 | private static final class StatusMatch extends CustomMatcher<Response> { | |
319 | /** | |
320 | * HTTP status to check. | |
321 | */ | |
322 | private final transient int status; | |
323 | /** | |
324 | * Ctor. | |
325 | * @param msg Message to show | |
326 | * @param sts HTTP status to check | |
327 | */ | |
328 | StatusMatch(final String msg, final int sts) { | |
329 | 114 | super(msg); |
330 | 114 | this.status = sts; |
331 | 114 | } |
332 | @Override | |
333 | public boolean matches(final Object resp) { | |
334 | 114 | return Response.class.cast(resp).status() == this.status; |
335 | } | |
336 | } | |
337 | ||
338 | } |