View Javadoc

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 }