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 }