1 package com.atlassian.marketplace.client.impl;
2
3 import java.io.ByteArrayInputStream;
4 import java.io.ByteArrayOutputStream;
5 import java.io.InputStream;
6 import java.io.OutputStream;
7 import java.net.URI;
8 import java.util.HashMap;
9 import java.util.Map;
10
11 import com.atlassian.fugue.Option;
12 import com.atlassian.fugue.Pair;
13 import com.atlassian.marketplace.client.http.HttpTransport;
14 import com.atlassian.marketplace.client.http.RequestDecorator;
15 import com.atlassian.marketplace.client.http.SimpleHttpResponse;
16 import com.atlassian.marketplace.client.model.ErrorDetail;
17 import com.atlassian.marketplace.client.model.ModelBuilders;
18 import com.atlassian.marketplace.client.util.UriBuilder;
19
20 import com.google.common.base.Joiner;
21 import com.google.common.collect.ImmutableList;
22 import com.google.common.collect.ImmutableMap;
23 import com.google.common.collect.Multimap;
24
25 import org.apache.commons.io.IOUtils;
26 import org.mockito.Mockito;
27 import org.mockito.invocation.InvocationOnMock;
28 import org.mockito.stubbing.Answer;
29
30 import static com.atlassian.fugue.Pair.pair;
31 import static com.atlassian.marketplace.client.TestObjects.API_V2_BASE_PATH;
32 import static com.atlassian.marketplace.client.TestObjects.HOST_BASE;
33 import static com.google.common.base.Preconditions.checkState;
34 import static org.mockito.Matchers.any;
35 import static org.mockito.Matchers.anyLong;
36 import static org.mockito.Matchers.anyString;
37 import static org.mockito.Matchers.eq;
38 import static org.mockito.Matchers.same;
39 import static org.mockito.Mockito.doAnswer;
40 import static org.mockito.Mockito.mock;
41 import static org.mockito.Mockito.when;
42
43 public class ClientTester
44 {
45 public static final String FAKE_ADDONS_PATH = "/fake/addons";
46 public static final String FAKE_APPLICATIONS_PATH = "/fake/applications";
47 public static final String FAKE_ASSETS_PATH = "/fake/assets";
48 public static final String FAKE_PRODUCTS_PATH = "/fake/products";
49 public static final String FAKE_VENDORS_PATH = "/fake/vendors";
50
51 final DefaultMarketplaceClient client;
52
53 public final URI apiBase;
54 public final HttpTransport httpTransport;
55 protected final EntityEncoding encoding;
56 protected final Map<Pair<URI, String>, SimpleHttpResponse> responses;
57 protected final Map<Pair<URI, String>, Object> receivedData;
58 protected final Map<Pair<URI, String>, ImmutableMap<String, String>> receivedHeaders;
59 protected final JsonEntityEncoding jsonEncoding = new JsonEntityEncoding();
60
61 public ClientTester(URI baseUri) throws Exception
62 {
63 this.apiBase = URI.create(HOST_BASE + API_V2_BASE_PATH + "/");
64
65 httpTransport = mock(HttpTransport.class);
66 encoding = mock(EntityEncoding.class);
67 responses = new HashMap<Pair<URI, String>, SimpleHttpResponse>();
68 receivedData = new HashMap<Pair<URI, String>, Object>();
69 receivedHeaders = new HashMap<Pair<URI, String>, ImmutableMap<String, String>>();
70
71 client = new DefaultMarketplaceClient(baseUri, httpTransport, encoding);
72
73 doAnswer(mockEncode()).when(encoding).encode(any(OutputStream.class), any(Object.class), Mockito.anyBoolean());
74 setupMockRequests(httpTransport, ImmutableMap.<String, String>of());
75 doAnswer(mockWithRequestDecorator()).when(httpTransport).withRequestDecorator(any(RequestDecorator.class));
76 }
77
78 @SuppressWarnings("unchecked")
79 private void setupMockRequests(HttpTransport ht, ImmutableMap<String, String> headers) throws Exception
80 {
81 doAnswer(mockHttpAnswer("DELETE", headers)).when(ht).delete(any(URI.class));
82 doAnswer(mockHttpAnswer("GET", headers)).when(ht).get(any(URI.class));
83 doAnswer(mockHttpAnswer("POST", headers)).when(ht).post(any(URI.class), any(InputStream.class), anyLong(),
84 anyString(), anyString());
85 doAnswer(mockHttpAnswer("POST", headers)).when(ht).postParams(any(URI.class), any(Multimap.class));
86 doAnswer(mockHttpAnswer("PUT", headers)).when(ht).put(any(URI.class), any(byte[].class));
87 doAnswer(mockHttpAnswer("PATCH", headers)).when(ht).patch(any(URI.class), any(byte[].class));
88 }
89
90 private Answer<SimpleHttpResponse> mockHttpAnswer(final String method, final ImmutableMap<String, String> headers)
91 {
92 return new Answer<SimpleHttpResponse>()
93 {
94 public SimpleHttpResponse answer(InvocationOnMock invocation) throws Throwable
95 {
96 URI uri = (URI) invocation.getArguments()[0];
97 Pair<URI, String> key = pair(uri, method);
98 if (!responses.containsKey(key))
99 {
100 throw new IllegalStateException("unexpected " + method + ": " + uri + " (expected: "
101 + Joiner.on(", ").join(responses.keySet()) + ")");
102 }
103
104 receivedHeaders.put(key, headers);
105
106 if (invocation.getArguments().length > 1)
107 {
108 receivedData.put(key, toReceivedData(invocation.getArguments()[1]));
109 }
110
111 return responses.get(key);
112 }
113 };
114 }
115
116 private Answer<HttpTransport> mockWithRequestDecorator()
117 {
118 return new Answer<HttpTransport>()
119 {
120 public HttpTransport answer(InvocationOnMock invocation) throws Throwable
121 {
122 RequestDecorator rd = (RequestDecorator) invocation.getArguments()[0];
123 HttpTransport mock = mock(HttpTransport.class);
124 setupMockRequests(mock, ImmutableMap.copyOf(rd.getRequestHeaders()));
125 return mock;
126 }
127 };
128 }
129
130 private Object toReceivedData(Object o) throws Exception
131 {
132 if (o instanceof InputStream)
133 {
134 return IOUtils.toByteArray((InputStream) o);
135 }
136 return o;
137 }
138
139 private Answer<Void> mockEncode()
140 {
141 return new Answer<Void>()
142 {
143 public Void answer(InvocationOnMock invocation) throws Throwable
144 {
145 OutputStream os = (OutputStream) invocation.getArguments()[0];
146 Object entity = invocation.getArguments()[1];
147 boolean includeReadOnly = (Boolean) invocation.getArguments()[2];
148 jsonEncoding.encode(os, entity, includeReadOnly);
149 return null;
150 }
151 };
152 }
153
154 private Answer<Void> mockEncodeChanges(final String content)
155 {
156 return new Answer<Void>()
157 {
158 public Void answer(InvocationOnMock invocation) throws Throwable
159 {
160 OutputStream os = (OutputStream) invocation.getArguments()[0];
161 os.write(content.getBytes());
162 return null;
163 }
164 };
165 }
166
167 public static InternalModel.MinimalLinks defaultRootResource()
168 {
169 return new InternalModel.MinimalLinks(createDefaultRootLinks().build());
170 }
171
172 public static InternalModel.MinimalLinks rootResourceMinusLink(String rel)
173 {
174 return new InternalModel.MinimalLinks(createDefaultRootLinks().remove(rel).build());
175 }
176
177 public static ModelBuilders.LinksBuilder createDefaultRootLinks()
178 {
179 return ModelBuilders.links()
180 .put("addons", URI.create(FAKE_ADDONS_PATH))
181 .put("applications", URI.create(FAKE_APPLICATIONS_PATH))
182 .put("assets", URI.create(FAKE_ASSETS_PATH))
183 .put("products", URI.create(FAKE_PRODUCTS_PATH))
184 .put("vendors", URI.create(FAKE_VENDORS_PATH));
185 }
186
187 public UriBuilder apiUri(String path)
188 {
189 return UriBuilder.fromUri(apiBase).path(path);
190 }
191
192 static String relativeApiPath(URI uri)
193 {
194 String s = uri.toString();
195 checkState(s.startsWith(HOST_BASE));
196 return s.substring(HOST_BASE.length());
197 }
198
199 SimpleHttpResponse mockResponse(int status, boolean isEmpty, InputStream mockStream) throws Exception
200 {
201 SimpleHttpResponse ret = mock(SimpleHttpResponse.class);
202 when(ret.getStatus()).thenReturn(status);
203 when(ret.isEmpty()).thenReturn(isEmpty);
204 when(ret.getContentStream()).thenReturn(mockStream);
205 when(ret.getHeader(Mockito.anyString())).thenReturn(ImmutableList.<String>of());
206 return ret;
207 }
208
209 @SuppressWarnings("unchecked")
210 public <T> void mockResource(URI uri, T rep) throws Exception
211 {
212 mockResource(uri, rep, (Class<T>)rep.getClass());
213 }
214
215 public <T> void mockResource(URI uri, T rep, Class<T> type) throws Exception
216 {
217 mockResourceInternal(uri, rep, type, "GET");
218 }
219
220 public <T> void mockEmptyResponse(URI uri) throws Exception
221 {
222 InputStream mockStream = mock(InputStream.class);
223 responses.put(pair(uri, "GET"), mockResponse(200, true, mockStream));
224 }
225
226 public <T> void mockDeleteResource(URI uri, int status) throws Exception
227 {
228 mockResourceStatusInternal(uri, status, "DELETE");
229 }
230
231 public <T> void mockPostResource(URI uri, int status) throws Exception
232 {
233 mockResourceStatusInternal(uri, status, "POST");
234 }
235
236 public <T> void mockPostResource(URI uri, int status, Map<String, Iterable<String>> headers) throws Exception
237 {
238 mockResourceStatusInternal(uri, status, "POST", headers);
239 }
240
241 @SuppressWarnings("unchecked")
242 public <T> void mockPostResourceResponse(URI uri, T responseRep) throws Exception
243 {
244 mockResourceInternal(uri, responseRep, (Class<T>)responseRep.getClass(), "POST");
245 }
246
247 public <T> void mockPutResource(URI uri, int status) throws Exception
248 {
249 mockResourceStatusInternal(uri, status, "PUT");
250 }
251
252 public <T> void mockPatchResource(URI uri, int status, Option<String> location) throws Exception
253 {
254 ImmutableMap.Builder<String, Iterable<String>> headers = ImmutableMap.builder();
255 for (String l: location)
256 {
257 headers.put("Location", ImmutableList.of(l));
258 }
259 mockResourceStatusInternal(uri, status, "PATCH", headers.build());
260 }
261
262 @SuppressWarnings("unchecked")
263 <T> void mockPutResourceResponse(URI uri, T responseRep) throws Exception
264 {
265 mockResourceInternal(uri, responseRep, (Class<T>)responseRep.getClass(), "PUT");
266 }
267
268 private <T> void mockResourceInternal(URI uri, T rep, Class<T> type, String method)
269 throws Exception
270 {
271 InputStream mockStream = mock(InputStream.class);
272 responses.put(pair(uri, method), mockResponse(200, false, mockStream));
273 when(encoding.decode(mockStream, type)).thenReturn(rep);
274 }
275
276 public void mockResourceErrorBody(URI uri, int status, Iterable<ErrorDetail> details, String method)
277 throws Exception
278 {
279 InternalModel.ErrorDetails ed = InternalModel.errorDetails(details);
280 ByteArrayOutputStream baos = new ByteArrayOutputStream();
281 jsonEncoding.encode(baos, ed, true);
282 mockResourceErrorBody(uri, status, new String(baos.toByteArray()), method);
283 when(encoding.decode(any(ByteArrayInputStream.class), eq(InternalModel.ErrorDetails.class)))
284 .thenReturn(ed);
285 }
286
287 public void mockResourceErrorBody(URI uri, int status, String body, String method)
288 throws Exception
289 {
290 InputStream stream = new ByteArrayInputStream(body.getBytes());
291 responses.put(pair(uri, method), mockResponse(status, false, stream));
292 }
293
294 public void mockResourceError(URI uri, int status) throws Exception
295 {
296 mockResourceStatusInternal(uri, status, "GET");
297 }
298
299 public void mockPostResourceError(URI uri, int status) throws Exception
300 {
301 mockResourceStatusInternal(uri, status, "POST");
302 }
303
304 public void mockResourceStatusInternal(URI uri, int status, String method) throws Exception
305 {
306 mockResourceStatusInternal(uri, status, method, ImmutableMap.<String, Iterable<String>>of());
307 }
308
309 @SuppressWarnings("unchecked")
310 void mockResourceStatusInternal(URI uri, int status, String method, Map<String, Iterable<String>> headers) throws Exception
311 {
312 InputStream mockStream = mock(InputStream.class);
313 SimpleHttpResponse resp = mockResponse(status, false, mockStream);
314 for (Map.Entry<String, Iterable<String>> h: headers.entrySet())
315 {
316 when(resp.getHeader(h.getKey())).thenReturn(h.getValue());
317 }
318 responses.put(pair(uri, method), resp);
319 when(encoding.decode(same(mockStream), any(Class.class))).thenThrow(
320 new IllegalStateException("encoding.decode should not have been called for this response"));
321 }
322
323 public void mockPatchDocument(Object original, Object updated, String document) throws Exception
324 {
325 doAnswer(mockEncodeChanges(document)).when(encoding).encodeChanges(any(OutputStream.class), same(original), same(updated));
326 }
327
328 public Object getReceivedObject(URI uri, String method) throws Exception
329 {
330 Pair<URI, String> key = pair(uri, method);
331 checkState(receivedData.containsKey(key));
332 return receivedData.get(key);
333 }
334
335 public String getReceivedData(URI uri, String method) throws Exception
336 {
337 return new String((byte[]) getReceivedObject(uri, method));
338 }
339
340 public ImmutableMap<String, String> getReceivedExtraHeaders(URI uri, String method) throws Exception
341 {
342 if (receivedHeaders.containsKey(pair(uri, method)))
343 {
344 return receivedHeaders.get(pair(uri, method));
345 }
346 return ImmutableMap.<String, String>of();
347 }
348 }