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.net.URI;
7   import java.util.regex.Pattern;
8   
9   import com.atlassian.fugue.Option;
10  import com.atlassian.marketplace.client.api.AddonCategoryId;
11  import com.atlassian.marketplace.client.api.AddonExternalLinkType;
12  import com.atlassian.marketplace.client.api.AddonVersionExternalLinkType;
13  import com.atlassian.marketplace.client.api.ApplicationKey;
14  import com.atlassian.marketplace.client.api.UriTemplate;
15  import com.atlassian.marketplace.client.api.VendorExternalLinkType;
16  import com.atlassian.marketplace.client.api.VendorId;
17  import com.atlassian.marketplace.client.model.Addon;
18  import com.atlassian.marketplace.client.model.AddonCategorySummary;
19  import com.atlassian.marketplace.client.model.AddonDistributionSummary;
20  import com.atlassian.marketplace.client.model.AddonPricing;
21  import com.atlassian.marketplace.client.model.AddonPricingItem;
22  import com.atlassian.marketplace.client.model.AddonReviewsSummary;
23  import com.atlassian.marketplace.client.model.AddonSummary;
24  import com.atlassian.marketplace.client.model.AddonVersion;
25  import com.atlassian.marketplace.client.model.AddonVersionStatus;
26  import com.atlassian.marketplace.client.model.AddonVersionSummary;
27  import com.atlassian.marketplace.client.model.Application;
28  import com.atlassian.marketplace.client.model.ApplicationStatus;
29  import com.atlassian.marketplace.client.model.ApplicationVersion;
30  import com.atlassian.marketplace.client.model.ApplicationVersionStatus;
31  import com.atlassian.marketplace.client.model.ConnectScope;
32  import com.atlassian.marketplace.client.model.Highlight;
33  import com.atlassian.marketplace.client.model.HtmlString;
34  import com.atlassian.marketplace.client.model.ImageInfo;
35  import com.atlassian.marketplace.client.model.LicenseEditionType;
36  import com.atlassian.marketplace.client.model.Link;
37  import com.atlassian.marketplace.client.model.Links;
38  import com.atlassian.marketplace.client.model.ModelBuilders;
39  import com.atlassian.marketplace.client.model.PaymentModel;
40  import com.atlassian.marketplace.client.model.Product;
41  import com.atlassian.marketplace.client.model.ProductVersion;
42  import com.atlassian.marketplace.client.model.Screenshot;
43  import com.atlassian.marketplace.client.model.Vendor;
44  import com.atlassian.marketplace.client.model.VendorSummary;
45  import com.atlassian.marketplace.client.model.VersionCompatibility;
46  import com.atlassian.utt.matchers.NamedFunction;
47  
48  import com.google.common.base.Function;
49  import com.google.common.collect.ImmutableList;
50  import com.google.common.collect.Iterables;
51  
52  import org.apache.commons.io.IOUtils;
53  import org.hamcrest.Matcher;
54  import org.joda.time.DateTime;
55  import org.joda.time.LocalDate;
56  import org.junit.Before;
57  import org.junit.Test;
58  
59  import static com.atlassian.fugue.Option.none;
60  import static com.atlassian.fugue.Option.some;
61  import static com.atlassian.marketplace.client.TestObjects.utcDateTime;
62  import static com.atlassian.marketplace.client.model.HtmlString.html;
63  import static com.atlassian.marketplace.client.model.ModelBuilders.links;
64  import static com.atlassian.marketplace.client.model.TestModelBuilders.connectScope;
65  import static com.atlassian.utt.matchers.NamedFunction.namedFunction;
66  import static com.atlassian.utt.reflect.ReflectionFunctions.accessor;
67  import static com.atlassian.utt.reflect.ReflectionFunctions.iterableAccessor;
68  import static org.hamcrest.MatcherAssert.assertThat;
69  import static org.hamcrest.Matchers.allOf;
70  import static org.hamcrest.Matchers.contains;
71  import static org.hamcrest.Matchers.equalTo;
72  
73  public class JsonEntityEncodingTest
74  {
75      private static final AddonCategoryId CATEGORY_ID = AddonCategoryId.fromUri(URI.create("/rest/categories/1"));
76      
77      private JsonEntityEncoding encoding;
78  
79      @Before
80      public void setUp()
81      {
82          encoding = new JsonEntityEncoding();
83      }
84  
85      @Test
86      public void canDecodeSingleLink() throws Exception
87      {
88          Links links = decode("v2/links", Links.class);
89          assertThat(links, hasLink("rel1", "uri1"));
90      }
91  
92      @Test
93      public void canDecodeLinkArray() throws Exception
94      {
95          Links links = decode("v2/links", Links.class);
96          assertThat(links, hasLinkArray("rel2", "uri2a", "uri2b"));
97      }
98  
99      @Test
100     public void canDecodeLinkTemplate() throws Exception
101     {
102         Links links = decode("v2/links", Links.class);
103         assertThat(links, hasLinkTemplate("rel3", "/template/{param}"));
104     }
105 
106     @Test
107     public void canEncodeSingleLink() throws Exception
108     {
109         Links links = ModelBuilders.links().put("rel1", URI.create("uri1")).build();
110         assertThat(encode(links), equalTo("{\"rel1\":{\"href\":\"uri1\"}}"));
111     }
112 
113     @Test
114     public void canEncodeLinkArray() throws Exception
115     {
116         Links links = ModelBuilders.links().put("rel1", ImmutableList.of(URI.create("uri1"),
117                                                                          URI.create("uri2"))).build();
118         assertThat(encode(links), equalTo("{\"rel1\":[{\"href\":\"uri1\"},{\"href\":\"uri2\"}]}"));
119     }
120 
121     @Test
122     public void canDecodeAddon() throws Exception
123     {
124         assertThat(decode("v2/addon", Addon.class), testAddon());
125     }
126 
127     @Test
128     public void canDecodeAddonCategorySummary() throws Exception
129     {
130         assertThat(decode("v2/addonCategorySummary", AddonCategorySummary.class), testAddonCategorySummary());
131     }
132 
133     @Test
134     public void canDecodeAddonDistributionSummaryWithoutInstalls() throws Exception
135     {
136         assertThat(decode("v2/addonDistributionSummary1",
137                           AddonDistributionSummary.class), testAddonDistributionSummary());
138     }
139 
140     @Test
141     public void canDecodeAddonDistributionSummaryWithInstalls() throws Exception
142     {
143         assertThat(decode("v2/addonDistributionSummary2", AddonDistributionSummary.class), testAddonDistributionSummary(
144             true));
145     }
146 
147     @Test
148     public void canDecodeAddonPricing() throws Exception
149     {
150         assertThat(decode("v2/addonPricing", AddonPricing.class), testAddonPricing());
151     }
152 
153     @Test
154     public void canDecodeAddonRoleBasedPricing() throws Exception
155     {
156         assertThat(decode("v2/addonRoleBasedPricing", AddonPricing.class), testAddonRoleBasedPricing());
157     }
158 
159     @Test
160     public void canDecodeAddonReviewsSummary() throws Exception
161     {
162         assertThat(decode("v2/addonReviewsSummary", AddonReviewsSummary.class), testAddonReviewsSummary());
163     }
164 
165     @Test
166     public void canDecodeAddonSummary() throws Exception
167     {
168         assertThat(decode("v2/addonSummary", AddonSummary.class), testAddonSummary());
169     }
170 
171     @Test
172     public void canDecodeAddonVersion() throws Exception
173     {
174         assertThat(decode("v2/addonVersion", AddonVersion.class), testAddonVersion());
175     }
176 
177     @Test
178     public void canDecodeAddonVersionSummary() throws Exception
179     {
180         assertThat(decode("v2/addonVersionSummary", AddonVersionSummary.class), testAddonVersionSummary());
181     }
182 
183     @Test
184     public void canDecodeApplication() throws Exception
185     {
186         assertThat(decode("v2/application", Application.class), testApplication());
187     }
188     
189     @Test
190     public void canDecodeApplicationVersion() throws Exception
191     {
192         assertThat(decode("v2/applicationVersion", ApplicationVersion.class), testApplicationVersion());
193     }
194     
195     @Test
196     public void canDecodeHighlightWithoutExplanation() throws Exception
197     {
198         assertThat(decode("v2/highlight1", Highlight.class), testHighlight());
199     }
200 
201     @Test
202     public void canDecodeHighlightWithExplanation() throws Exception
203     {
204         assertThat(decode("v2/highlight2", Highlight.class), testHighlight(true));
205     }
206 
207     @Test
208     public void canDecodeProduct() throws Exception
209     {
210         assertThat(decode("v2/product", Product.class), testProduct());
211     }
212 
213     @Test
214     public void canDecodeProductVersion() throws Exception
215     {
216         assertThat(decode("v2/productVersion", ProductVersion.class), testProductVersion());
217     }
218 
219     @Test
220     public void canDecodeScreenshotWithoutCaption() throws Exception
221     {
222         assertThat(decode("v2/screenshot1", Screenshot.class), testScreenshot());
223     }
224 
225     @Test
226     public void canDecodeScreenshotWithCaption() throws Exception
227     {
228         assertThat(decode("v2/screenshot2", Screenshot.class), testScreenshot(true));
229     }
230 
231     @Test
232     public void canDecodeVendor() throws Exception
233     {
234         assertThat(decode("v2/vendor", Vendor.class), testVendor());
235     }
236 
237     @Test
238     public void canDecodeVendorSummary() throws Exception
239     {
240         assertThat(decode("v2/vendorSummary", VendorSummary.class), testVendorSummary());
241     }
242 
243     @Test
244     public void patchIsEmptyForUnchangedObject() throws Exception
245     {
246         ConnectScope o = connectScope("key", "name", "desc");
247         assertThat(encodeChanges(o, o), equalTo("[]"));
248     }
249     
250     @Test
251     public void patchHasReplaceForModifiedTopLevelProperty() throws Exception
252     {
253         ConnectScope o0 = connectScope("key", "name0", "desc");
254         ConnectScope o1 = connectScope("key", "name1", "desc");
255         assertThat(encodeChanges(o0, o1),
256             equalTo("[{\"op\":\"replace\",\"path\":\"/name\",\"value\":\"name1\"}]"));
257     }
258 
259     @Test
260     public void patchHasAddForNewTopLevelProperty() throws Exception
261     {
262         Links links = links().put("rel0", URI.create("/url")).put("rel1", some("foo"), URI.create("/url")).build();
263         Link link0 = links.getLink("rel0").get();
264         Link link1 = links.getLink("rel1").get();
265         assertThat(encodeChanges(link0, link1),
266             equalTo("[{\"op\":\"add\",\"path\":\"/type\",\"value\":\"foo\"}]"));
267     }
268 
269     @Test
270     public void patchHasRemoveForRemovedTopLevelProperty() throws Exception
271     {
272         Links links = links().put("rel0", URI.create("/url")).put("rel1", some("foo"), URI.create("/url")).build();
273         Link link0 = links.getLink("rel0").get();
274         Link link1 = links.getLink("rel1").get();
275         assertThat(encodeChanges(link1, link0),
276             equalTo("[{\"op\":\"remove\",\"path\":\"/type\"}]"));
277     }
278 
279     @Test
280     public void patchHasReplaceForModifiedNestedProperty() throws Exception
281     {
282         Links links0 = links().put("rel0", URI.create("/url")).put("rel1", URI.create("/url")).build();
283         Links links1 = links().put("rel0", URI.create("/url")).put("rel1", URI.create("/url2")).build();
284         assertThat(encodeChanges(links0, links1),
285             equalTo("[{\"op\":\"replace\",\"path\":\"/rel1/href\",\"value\":\"/url2\"}]"));
286     }
287 
288     @Test
289     public void patchHasAddForAddedNestedProperty() throws Exception
290     {
291         Links links0 = links().put("rel0", URI.create("/url")).put("rel1", URI.create("/url")).build();
292         Links links1 = links().put("rel0", URI.create("/url")).put("rel1", some("foo"), URI.create("/url")).build();
293         assertThat(encodeChanges(links0, links1),
294             equalTo("[{\"op\":\"add\",\"path\":\"/rel1/type\",\"value\":\"foo\"}]"));
295     }
296 
297     @Test
298     public void patchHasRemoveForRemovedNestedProperty() throws Exception
299     {
300         Links links0 = links().put("rel0", URI.create("/url")).put("rel1", URI.create("/url")).build();
301         Links links1 = links().put("rel0", URI.create("/url")).put("rel1", some("foo"), URI.create("/url")).build();
302         assertThat(encodeChanges(links1, links0),
303             equalTo("[{\"op\":\"remove\",\"path\":\"/rel1/type\"}]"));
304     }
305 
306     @Test
307     public void patchHasReplaceForEntireArrayIfPropertyOfElementIsModified() throws Exception
308     {
309         Links links0 = links().put("rel", ImmutableList.of(URI.create("/url0"), URI.create("/url1"))).build();
310         Links links1 = links().put("rel", ImmutableList.of(URI.create("/url0"), URI.create("/url2"))).build();
311         assertThat(encodeChanges(links0, links1),
312             equalTo("[{\"op\":\"replace\",\"path\":\"/rel\",\"value\":[{\"href\":\"/url0\"},{\"href\":\"/url2\"}]}]"));
313     }
314 
315     @SuppressWarnings({ "unchecked", "deprecation" })
316     private static Matcher<Addon> testAddon()
317     {
318         return allOf(
319             accessor(Addon.class, String.class, "getKey").is("plugin.key"),
320             accessor(Addon.class, String.class, "getName").is("Addon Name"),
321             iterableAccessor(Addon.class, String.class, "getSummary").is(some("addon summary")),
322             accessor(Addon.class, URI.class, "getAlternateUri").is(URI.create("http://marketplace.atlassian.com/addon/plugin.key")),
323             accessor(Addon.class, VendorId.class, "getVendorId").is(VendorId.fromUri(URI.create("http://marketplace.atlassian.com/vendors/1"))),
324             iterableAccessor(Addon.class, ImageInfo.class, "getLogo").is(contains(testImageAsset())),
325             iterableAccessor(Addon.class, ImageInfo.class, "getBanner").is(contains(testImageAsset())),
326             iterableAccessor(Addon.class, AddonCategorySummary.class, "getCategories").is(contains(testAddonCategorySummary())),
327             iterableAccessor(Addon.class, AddonCategoryId.class, "getCategoryIds").is(contains(CATEGORY_ID)),
328             accessor(Addon.class, AddonDistributionSummary.class, "getDistribution").is(testAddonDistributionSummary(true)),
329             accessor(Addon.class, AddonReviewsSummary.class, "getReviews").is(testAddonReviewsSummary()),
330             iterableAccessor(Addon.class, VendorSummary.class, "getVendor").is(contains(testVendorSummary())),
331             iterableAccessor(Addon.class, AddonVersion.class, "getVersion").is(contains(testAddonVersion())),
332             iterableAccessor(Addon.class, HtmlString.class, "getDescription").is(some(html("addon description"))),
333             addonUri(AddonExternalLinkType.ISSUE_TRACKER).is(some(URI.create("/issueTracker"))),
334             addonUri(AddonExternalLinkType.FORUMS).is(some(URI.create("/forums"))),
335             addonUri(AddonExternalLinkType.PRIVACY).is(some(URI.create("/privacy"))),
336             addonUri(AddonExternalLinkType.WIKI).is(some(URI.create("/wiki"))),
337             addonUri(AddonExternalLinkType.SOURCE).is(some(URI.create("/source"))),
338             addonUri(AddonExternalLinkType.BUILDS).is(some(URI.create("/builds")))
339         );
340     }
341     private static NamedFunction<Addon, Option<URI>> addonUri(final AddonExternalLinkType type)
342     {
343         return namedFunction(type.getKey() + "Uri", new Function<Addon, Option<URI>>()
344         {
345             public Option<URI> apply(Addon a)
346             {
347                 return a.getExternalLinkUri(type);
348             }
349         });
350     }
351     
352     private static Matcher<AddonCategorySummary> testAddonCategorySummary()
353     {
354         return allOf(
355             accessor(AddonCategorySummary.class, Links.class, "getLinks").is(hasLink("self", CATEGORY_ID.getUri().toString())),
356             accessor(AddonCategorySummary.class, AddonCategoryId.class, "getId").is(CATEGORY_ID),
357             accessor(AddonCategorySummary.class, String.class, "getName").is("my-name")
358         );
359     }
360 
361     private static Matcher<AddonDistributionSummary> testAddonDistributionSummary()
362     {
363         return testAddonDistributionSummary(false);
364     }
365 
366     private static Matcher<AddonDistributionSummary> testAddonDistributionSummary(boolean withInstalls)
367     {
368         return allOf(
369             accessor(AddonDistributionSummary.class, boolean.class, "isBundled").is(true),
370             accessor(AddonDistributionSummary.class, int.class, "getDownloads").is(1000),
371             iterableAccessor(AddonDistributionSummary.class, Integer.class, "getTotalInstalls").is(withInstalls ? some(200) : none(Integer.class)),
372             iterableAccessor(AddonDistributionSummary.class, Integer.class, "getTotalUsers").is(withInstalls ? some(100) : none(Integer.class))
373         );
374     }
375 
376     @SuppressWarnings("unchecked")
377     private static Matcher<AddonPricing> testAddonPricing()
378     {
379         return allOf(
380             accessor(AddonPricing.class, Links.class, "getLinks").is(hasLink("self", "/rest/plugins/plugin.key/pricing")),
381             iterableAccessor(AddonPricing.class, AddonPricingItem.class, "getItems").is(contains(testAddonPricingItem())),
382             accessor(AddonPricing.class, boolean.class, "isExpertDiscountOptOut").is(true),
383             accessor(AddonPricing.class, boolean.class, "isContactSalesForAdditionalPricing").is(false),
384             accessor(AddonPricing.class, boolean.class, "isRoleBased").is(false),
385             iterableAccessor(AddonPricing.class, String.class, "getParent").is(some("jira")),
386             iterableAccessor(AddonPricing.class, DateTime.class, "getLastModified").is(some(utcDateTime(2015, 2, 10, 1, 2, 3)))
387         );
388     }
389 
390     @SuppressWarnings("unchecked")
391     private static Matcher<AddonPricing> testAddonRoleBasedPricing()
392     {
393         return allOf(
394             accessor(AddonPricing.class, Links.class, "getLinks").is(hasLink("self", "/rest/plugins/plugin.key/pricing")),
395             iterableAccessor(AddonPricing.class, AddonPricingItem.class, "getItems").is(contains(testAddonRoleBasedPricingItem())),
396             accessor(AddonPricing.class, boolean.class, "isExpertDiscountOptOut").is(true),
397             accessor(AddonPricing.class, boolean.class, "isContactSalesForAdditionalPricing").is(true),
398             accessor(AddonPricing.class, boolean.class, "isRoleBased").is(true),
399             iterableAccessor(AddonPricing.class, String.class, "getParent").is(some("jira")),
400             iterableAccessor(AddonPricing.class, DateTime.class, "getLastModified").is(some(utcDateTime(2015, 2, 10, 1, 2, 3))),
401             iterableAccessor(AddonPricing.class, AddonPricing.RoleInfo.class, "getRoleInfo").is(contains(testAddonPricingRole()))
402         );
403     }
404 
405     @SuppressWarnings("unchecked")
406     private static Matcher<AddonPricingItem> testAddonPricingItem()
407     {
408         return allOf(
409             accessor(AddonPricingItem.class, String.class, "getDescription").is("Commercial 25 Users"),
410             accessor(AddonPricingItem.class, String.class, "getEditionId").is("userTier"),
411             accessor(AddonPricingItem.class, String.class, "getEditionDescription").is("Users"),
412             accessor(AddonPricingItem.class, LicenseEditionType.class, "getEditionType").is(LicenseEditionType.USER_TIER),
413             accessor(AddonPricingItem.class, String.class, "getLicenseType").is("COMMERCIAL"),
414             accessor(AddonPricingItem.class, float.class, "getAmount").is(250.00f),
415             iterableAccessor(AddonPricingItem.class, Float.class, "getRenewalAmount").is(contains(125.00f)),
416             accessor(AddonPricingItem.class, int.class, "getUnitCount").is(25),
417             accessor(AddonPricingItem.class, int.class, "getMonthsValid").is(12)
418         );
419     }
420 
421     @SuppressWarnings("unchecked")
422     private static Matcher<AddonPricingItem> testAddonRoleBasedPricingItem()
423     {
424         return allOf(
425             accessor(AddonPricingItem.class, String.class, "getDescription").is("Commercial 25 Users"),
426             accessor(AddonPricingItem.class, String.class, "getEditionId").is("userTier"),
427             accessor(AddonPricingItem.class, String.class, "getEditionDescription").is("Users"),
428             accessor(AddonPricingItem.class, LicenseEditionType.class, "getEditionType").is(LicenseEditionType.ROLE_TIER),
429             accessor(AddonPricingItem.class, String.class, "getLicenseType").is("COMMERCIAL"),
430             accessor(AddonPricingItem.class, float.class, "getAmount").is(250.00f),
431             iterableAccessor(AddonPricingItem.class, Float.class, "getRenewalAmount").is(contains(125.00f)),
432             accessor(AddonPricingItem.class, int.class, "getUnitCount").is(25),
433             accessor(AddonPricingItem.class, int.class, "getMonthsValid").is(12)
434         );
435     }
436 
437     private static Matcher<AddonPricing.RoleInfo> testAddonPricingRole()
438     {
439         return allOf(
440             accessor(AddonPricing.RoleInfo.class, String.class, "getSingularName").is("Agent"),
441             accessor(AddonPricing.RoleInfo.class, String.class, "getPluralName").is("Agents")
442         );
443     }
444 
445     private static Matcher<AddonReviewsSummary> testAddonReviewsSummary()
446     {
447         return allOf(
448             accessor(AddonReviewsSummary.class, float.class, "getAverageStars").is(3.5f),
449             accessor(AddonReviewsSummary.class, int.class, "getCount").is(1000)
450         );
451     }
452 
453     @SuppressWarnings("unchecked")
454     private static Matcher<AddonSummary> testAddonSummary()
455     {
456         return allOf(
457             accessor(AddonSummary.class, String.class, "getKey").is("plugin.key"),
458             accessor(AddonSummary.class, String.class, "getName").is("Addon Name"),
459             iterableAccessor(AddonSummary.class, String.class, "getSummary").is(some("addon summary")),
460             accessor(AddonSummary.class, URI.class, "getAlternateUri").is(URI.create("http://marketplace.atlassian.com/addon/plugin.key")),
461             accessor(AddonSummary.class, VendorId.class, "getVendorId").is(VendorId.fromUri(URI.create("http://marketplace.atlassian.com/vendors/1"))),
462             iterableAccessor(AddonSummary.class, AddonCategorySummary.class, "getCategories").is(contains(testAddonCategorySummary())),
463             iterableAccessor(AddonSummary.class, AddonCategoryId.class, "getCategoryIds").is(contains(CATEGORY_ID)),
464             accessor(AddonSummary.class, AddonDistributionSummary.class, "getDistribution").is(testAddonDistributionSummary(true)),
465             iterableAccessor(AddonSummary.class, ImageInfo.class, "getLogo").is(contains(testImageAsset())),
466             accessor(AddonSummary.class, AddonReviewsSummary.class, "getReviews").is(testAddonReviewsSummary()),
467             iterableAccessor(AddonSummary.class, VendorSummary.class, "getVendor").is(contains(testVendorSummary())),
468             iterableAccessor(AddonSummary.class, AddonVersionSummary.class, "getVersion").is(contains(testAddonVersionSummary()))
469         );
470     }
471 
472     @SuppressWarnings({ "unchecked", "deprecation" })
473     private static Matcher<AddonVersion> testAddonVersion()
474     {
475         return allOf(
476             accessor(AddonVersion.class, int.class, "getBuildNumber").is(100),
477             iterableAccessor(AddonVersion.class, String.class, "getName").is(some("1.0")),
478             accessor(AddonVersion.class, AddonVersionStatus.class, "getStatus").is(AddonVersionStatus.PUBLIC),
479             accessor(AddonVersion.class, PaymentModel.class, "getPaymentModel").is(PaymentModel.PAID_VIA_ATLASSIAN),
480             testAddonVersionReleaseProperties(AddonVersion.class),
481             accessor(AddonVersion.class, boolean.class, "isStatic").is(false),
482             accessor(AddonVersion.class, boolean.class, "isDeployable").is(true),
483             iterableAccessor(AddonVersion.class, String.class, "getYoutubeId").is(some("abc")),
484             addonVersionUri(AddonVersionExternalLinkType.BINARY).is(some(URI.create("/binary"))),
485             addonVersionUri(AddonVersionExternalLinkType.DOCUMENTATION).is(some(URI.create("/documentation"))),
486             addonVersionUri(AddonVersionExternalLinkType.LICENSE).is(some(URI.create("/license"))),
487             addonVersionUri(AddonVersionExternalLinkType.LEARN_MORE).is(some(URI.create("/learnMore"))),
488             addonVersionUri(AddonVersionExternalLinkType.EULA).is(some(URI.create("/eula"))),
489             addonVersionUri(AddonVersionExternalLinkType.PURCHASE).is(some(URI.create("/purchase"))),
490             addonVersionUri(AddonVersionExternalLinkType.RELEASE_NOTES).is(some(URI.create("/releaseNotes"))),
491             addonVersionUri(AddonVersionExternalLinkType.JAVADOC).is(some(URI.create("/javadocs"))),
492             addonVersionUri(AddonVersionExternalLinkType.SOURCE).is(some(URI.create("/source"))),
493             addonVersionUri(AddonVersionExternalLinkType.EVALUATION_LICENSE).is(some(URI.create("/eval"))),
494             addonVersionUri(AddonVersionExternalLinkType.DONATE).is(some(URI.create("/donate"))),
495             iterableAccessor(AddonVersion.class, VersionCompatibility.class, "getCompatibilities").is(contains(testVersionCompatibility())),
496             iterableAccessor(AddonVersion.class, AddonCategorySummary.class, "getFunctionalCategories").is(contains(testAddonCategorySummary())),
497             iterableAccessor(AddonVersion.class, AddonCategoryId.class, "getFunctionalCategoryIds").is(contains(CATEGORY_ID)),
498             iterableAccessor(AddonVersion.class, Highlight.class, "getHighlights").is(contains(testHighlight(false), testHighlight(true))),
499             iterableAccessor(AddonVersion.class, Screenshot.class, "getScreenshots").is(contains(testScreenshot(false), testScreenshot(true))),
500             iterableAccessor(AddonVersion.class, String.class, "getReleaseSummary").is(some("rs")),
501             iterableAccessor(AddonVersion.class, HtmlString.class, "getMoreDetails").is(some(html("md"))),
502             iterableAccessor(AddonVersion.class, HtmlString.class, "getReleaseNotes").is(some(html("rn"))),
503             iterableAccessor(AddonVersion.class, ConnectScope.class, "getConnectScopes").is(contains(testConnectScope()))
504         );
505     }
506 
507     private static NamedFunction<AddonVersion, Option<URI>> addonVersionUri(final AddonVersionExternalLinkType type)
508     {
509         return namedFunction(type.getKey() + "Uri", new Function<AddonVersion, Option<URI>>()
510         {
511             public Option<URI> apply(AddonVersion v)
512             {
513                 return v.getExternalLinkUri(type);
514             }
515         });
516     }
517     
518     private static <T> Matcher<T> testAddonVersionReleaseProperties(Class<T> entityClass)
519     {
520         return allOf(
521             accessor(entityClass, LocalDate.class, "getReleaseDate").is(new LocalDate(2014, 05, 24)),
522             iterableAccessor(entityClass, String.class, "getReleasedBy").is(some("me")),
523             accessor(entityClass, boolean.class, "isBeta").is(false),
524             accessor(entityClass, boolean.class, "isSupported").is(true)
525         );
526     }
527     
528     @SuppressWarnings("unchecked")
529     private static Matcher<AddonVersionSummary> testAddonVersionSummary()
530     {
531         return allOf(
532             iterableAccessor(AddonVersionSummary.class, String.class, "getName").is(some("1.0")),
533             accessor(AddonVersionSummary.class, AddonVersionStatus.class, "getStatus").is(AddonVersionStatus.PUBLIC),
534             accessor(AddonVersionSummary.class, PaymentModel.class, "getPaymentModel").is(PaymentModel.PAID_VIA_ATLASSIAN),
535             testAddonVersionReleaseProperties(AddonVersionSummary.class),
536             accessor(AddonVersionSummary.class, boolean.class, "isStatic").is(false),
537             accessor(AddonVersionSummary.class, boolean.class, "isDeployable").is(true),
538             accessor(AddonVersionSummary.class, boolean.class, "isServer").is(true),
539             accessor(AddonVersionSummary.class, boolean.class, "isCloud").is(false),
540             accessor(AddonVersionSummary.class, boolean.class, "isConnect").is(false),
541             accessor(AddonVersionSummary.class, boolean.class, "isAutoUpdateAllowed").is(false),
542             accessor(AddonVersionSummary.class, boolean.class, "isDataCenterCompatible").is(true),
543             iterableAccessor(AddonVersionSummary.class, AddonCategorySummary.class, "getFunctionalCategories").is(contains(testAddonCategorySummary())),
544             iterableAccessor(AddonVersionSummary.class, AddonCategoryId.class, "getFunctionalCategoryIds").is(contains(CATEGORY_ID))
545         );
546     }
547     
548     @SuppressWarnings("unchecked")
549     private static Matcher<Application> testApplication()
550     {
551         return allOf(
552             accessor(Application.class, ApplicationKey.class, "getKey").is(ApplicationKey.JIRA),
553             accessor(Application.class, String.class, "getName").is("JIRA"),
554             accessor(Application.class, ApplicationStatus.class, "getStatus").is(ApplicationStatus.PUBLISHED),
555             accessor(Application.class, String.class, "getIntroduction").is("yo"),
556             accessor(Application.class, Application.CompatibilityUpdateMode.class, "getCompatibilityUpdateMode").is(Application.CompatibilityUpdateMode.MINOR_VERSIONS),
557             accessor(Application.class, String.class, "getDescription").is("it's good"),
558             accessor(Application.class, URI.class, "getLearnMoreUri").is(URI.create("http://learn/more")),
559             iterableAccessor(Application.class, URI.class, "getDownloadPageUri").is(some(URI.create("http://download/it"))),
560             iterableAccessor(Application.class, Integer.class, "getCloudFreeUsers").is(some(5))
561         );
562     }
563     
564     private static Matcher<ApplicationVersion> testApplicationVersion()
565     {
566         return allOf(
567             accessor(ApplicationVersion.class, int.class, "getBuildNumber").is(100),
568             accessor(ApplicationVersion.class, String.class, "getName").is("1.0"),
569             accessor(ApplicationVersion.class, LocalDate.class, "getReleaseDate").is(new LocalDate(2014, 05, 24)),
570             accessor(ApplicationVersion.class, ApplicationVersionStatus.class, "getStatus").is(ApplicationVersionStatus.PUBLISHED)
571         );
572     }
573     
574     private static Matcher<ConnectScope> testConnectScope()
575     {
576         return allOf(
577             accessor(ConnectScope.class, URI.class, "getAlternateUri").is(URI.create("/permissions/read")),
578             accessor(ConnectScope.class, String.class, "getKey").is("read"),
579             accessor(ConnectScope.class, String.class, "getName").is("Read"),
580             accessor(ConnectScope.class, String.class, "getDescription").is("Read things")
581         );
582     }
583     
584     private static Matcher<Highlight> testHighlight()
585     {
586         return testHighlight(false);
587     }
588 
589     private static Matcher<Highlight> testHighlight(boolean withExplanation)
590     {
591         return allOf(
592             accessor(Highlight.class, ImageInfo.class, "getFullImage").is(testImageAsset("big")),
593             accessor(Highlight.class, ImageInfo.class, "getThumbnailImage").is(testImageAsset("small")),
594             accessor(Highlight.class, String.class, "getTitle").is("my-title"),
595             accessor(Highlight.class, HtmlString.class, "getBody").is(html("my-body")),
596             iterableAccessor(Highlight.class, String.class, "getExplanation").is(withExplanation ? some("my-explanation") : none(String.class))
597         );
598     }
599 
600     private static Matcher<ImageInfo> testImageAsset()
601     {
602         return testImageAsset("image");
603     }
604 
605     private static Matcher<ImageInfo> testImageAsset(String name)
606     {
607         return allOf(
608             accessor(ImageInfo.class, URI.class, "getImageUri").is(URI.create("/" + name)),
609             imageUriWithParams(ImageInfo.Size.SMALL_SIZE, ImageInfo.Resolution.DEFAULT_RESOLUTION).is(none(URI.class)),
610             imageUriWithParams(ImageInfo.Size.DEFAULT_SIZE, ImageInfo.Resolution.HIGH_RESOLUTION).is(some(URI.create("/" + name + "/hi"))),
611             imageUriWithParams(ImageInfo.Size.SMALL_SIZE, ImageInfo.Resolution.HIGH_RESOLUTION).is(none(URI.class))
612         );
613     }
614 
615     private static Matcher<Screenshot> testScreenshot()
616     {
617         return testScreenshot(false);
618     }
619 
620     private static Matcher<Screenshot> testScreenshot(boolean withCaption)
621     {
622         return allOf(
623             accessor(Screenshot.class, ImageInfo.class, "getImage").is(testImageAsset()),
624             iterableAccessor(Screenshot.class, String.class, "getCaption").is(withCaption ? some("my-caption") : none(String.class))
625         );
626     }
627 
628     private static NamedFunction<ImageInfo, Option<URI>> imageUriWithParams(final ImageInfo.Size size, final ImageInfo.Resolution resolution)
629     {
630         return namedFunction("imageUriWithParams(" + size + "," + resolution + ")",
631                 new Function<ImageInfo, Option<URI>>()
632                 {
633                     public Option<URI> apply(ImageInfo i)
634                     {
635                         return i.getImageUri(size, resolution);
636                     }
637                 });
638     }
639 
640     private static Matcher<Product> testProduct()
641     {
642         return allOf(
643             accessor(Product.class, String.class, "getKey").is("my-key"),
644             accessor(Product.class, String.class, "getName").is("my-name"),
645             accessor(Product.class, String.class, "getSummary").is("my-summary"),
646             iterableAccessor(Product.class, ImageInfo.class, "getLogo").is(contains(testImageAsset())),
647             iterableAccessor(Product.class, ProductVersion.class, "getVersion").is(contains(testProductVersion()))
648         );        
649     }
650     
651     @SuppressWarnings("unchecked")
652     private static Matcher<ProductVersion> testProductVersion()
653     {
654         return allOf(
655             accessor(ProductVersion.class, String.class, "getName").is("1.0"),
656             accessor(ProductVersion.class, int.class, "getBuildNumber").is(100),
657             accessor(ProductVersion.class, PaymentModel.class, "getPaymentModel").is(PaymentModel.PAID_VIA_ATLASSIAN),
658             iterableAccessor(ProductVersion.class, URI.class, "getArtifactUri").is(contains(URI.create("/binary"))),
659             iterableAccessor(ProductVersion.class, URI.class, "getLearnMoreUri").is(contains(URI.create("/learn-more"))),
660             accessor(ProductVersion.class, LocalDate.class, "getReleaseDate").is(new LocalDate(2012, 04, 01)),
661             iterableAccessor(ProductVersion.class, VersionCompatibility.class, "getCompatibilities").is(contains(testVersionCompatibility()))
662         );
663     }
664 
665     @SuppressWarnings("unchecked")
666     private static Matcher<Vendor> testVendor()
667     {
668         return allOf(
669             accessor(Vendor.class, URI.class, "getSelfUri").is(URI.create("/rest/vendors/1")),
670             accessor(Vendor.class, URI.class, "getAlternateUri").is(URI.create("http://marketplace.atlassian.com/vendors/1")),
671             accessor(Vendor.class, String.class, "getName").is("my-name"),
672             iterableAccessor(Vendor.class, String.class, "getDescription").is(some("desc")),
673             accessor(Vendor.class, String.class, "getEmail").is("test@example.com"),
674             iterableAccessor(Vendor.class, String.class, "getPhone").is(some("phone")),
675             iterableAccessor(Vendor.class, ImageInfo.class, "getLogo").is(contains(testImageAsset())),
676             vendorUri(VendorExternalLinkType.HOME_PAGE).is(some(URI.create("http://home"))),
677             vendorUri(VendorExternalLinkType.SLA).is(some(URI.create("http://sla"))),
678             iterableAccessor(Vendor.class, String.class, "otherContactDetails").is(some("contact")),
679             accessor(Vendor.class, boolean.class, "isVerified").is(true)
680         );
681     }
682     
683     private static NamedFunction<Vendor, Option<URI>> vendorUri(final VendorExternalLinkType type)
684     {
685         return NamedFunction.namedFunction(type.getKey() + "Uri", new Function<Vendor, Option<URI>>()
686         {
687             public Option<URI> apply(Vendor v)
688             {
689                 return v.getExternalLinkUri(type);
690             }
691         });
692     }
693     
694     private static Matcher<VendorSummary> testVendorSummary()
695     {
696         return allOf(
697             accessor(VendorSummary.class, URI.class, "getSelfUri").is(URI.create("/rest/vendors/1")),
698             accessor(VendorSummary.class, URI.class, "getAlternateUri").is(URI.create("http://marketplace.atlassian.com/vendors/1")),
699             accessor(VendorSummary.class, String.class, "getName").is("my-name"),
700             iterableAccessor(VendorSummary.class, ImageInfo.class, "getLogo").is(contains(testImageAsset()))
701         );
702     }
703     
704     private static Matcher<VersionCompatibility> testVersionCompatibility()
705     {
706         return allOf(
707             accessor(VersionCompatibility.class, ApplicationKey.class, "getApplication").is(ApplicationKey.JIRA),
708             iterableAccessor(VersionCompatibility.class, Integer.class, "getServerMinBuild").is(contains(100)),
709             iterableAccessor(VersionCompatibility.class, Integer.class, "getServerMaxBuild").is(contains(200)),
710             accessor(VersionCompatibility.class, boolean.class, "isCloudCompatible").is(true)
711         );
712     }
713 
714     private static Matcher<Links> hasLink(final String rel, String href)
715     {
716         return NamedFunction.namedFunction("getLink(" + rel + ")", new Function<Links, Option<URI>>()
717         {
718             public Option<URI> apply(Links input)
719             {
720                 return input.getUri(rel);
721             }
722         }).is(some(URI.create(href)));
723     }
724 
725     private static Matcher<Links> hasLinkArray(final String rel, String... href)
726     {
727         return NamedFunction.namedFunction("getLinks(" + rel + ")", new Function<Links, Iterable<String>>()
728         {
729             public Iterable<String> apply(Links input)
730             {
731                 return Iterables.transform(input.getLinks(rel), new Function<Link, String>()
732                 {
733                     public String apply(Link input)
734                     {
735                         return input.stringValue();
736                     }
737                 });
738             }
739         }).is(contains(href));
740     }
741 
742     private static Matcher<Links> hasLinkTemplate(final String rel, String template)
743     {
744         return NamedFunction.namedFunction("getLink(" + rel + ")", new Function<Links, Option<UriTemplate>>()
745         {
746             public Option<UriTemplate> apply(Links input)
747             {
748                 return input.getUriTemplate(rel);
749             }
750         }).is(some(UriTemplate.create(template)));
751     }
752 
753     private <T> T decode(String filename, Class<T> type) throws Exception
754     {
755         return encoding.decode(new ByteArrayInputStream(readStringResource(filename + ".json").getBytes()), type);
756     }
757     
758     private String readStringResource(String filename) throws Exception
759     {
760         InputStream is = getClass().getClassLoader().getResourceAsStream(filename);
761         if (is == null)
762         {
763             throw new IllegalStateException("missing test resource file " + filename);
764         }
765         String s = IOUtils.toString(is);
766         Pattern include = Pattern.compile("\"#include\\(([^)]*)\\)\"");
767         while (true)
768         {
769             java.util.regex.Matcher m = include.matcher(s);
770             if (!m.find())
771             {
772                 return s;
773             }
774             s = m.replaceFirst(readStringResource(m.group(1)));
775         }
776     }
777     
778     private String encode(Object entity) throws Exception
779     {
780         ByteArrayOutputStream os = new ByteArrayOutputStream();
781         encoding.encode(os, entity, false);
782         return os.toString();
783     }
784     
785     private String encodeChanges(Object e1, Object e2) throws Exception
786     {
787         ByteArrayOutputStream os = new ByteArrayOutputStream();
788         encoding.encodeChanges(os, e1, e2);
789         return os.toString();
790     }
791 }