Use real image parsing in Sone downloader test.
[Sone.git] / src / test / java / net / pterodactylus / sone / core / SoneDownloaderTest.java
1 package net.pterodactylus.sone.core;
2
3 import static com.google.common.base.Optional.of;
4 import static java.lang.System.currentTimeMillis;
5 import static java.util.UUID.randomUUID;
6 import static java.util.concurrent.TimeUnit.DAYS;
7 import static net.pterodactylus.sone.data.Sone.SoneStatus.downloading;
8 import static net.pterodactylus.sone.data.Sone.SoneStatus.idle;
9 import static net.pterodactylus.sone.data.Sone.SoneStatus.unknown;
10 import static org.hamcrest.MatcherAssert.assertThat;
11 import static org.hamcrest.Matchers.hasSize;
12 import static org.hamcrest.Matchers.is;
13 import static org.hamcrest.Matchers.not;
14 import static org.hamcrest.Matchers.notNullValue;
15 import static org.hamcrest.Matchers.nullValue;
16 import static org.mockito.ArgumentCaptor.forClass;
17 import static org.mockito.Matchers.any;
18 import static org.mockito.Matchers.anyLong;
19 import static org.mockito.Matchers.anyString;
20 import static org.mockito.Matchers.eq;
21 import static org.mockito.Mockito.doAnswer;
22 import static org.mockito.Mockito.mock;
23 import static org.mockito.Mockito.never;
24 import static org.mockito.Mockito.times;
25 import static org.mockito.Mockito.verify;
26 import static org.mockito.Mockito.when;
27
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.net.MalformedURLException;
31 import java.util.ArrayList;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Set;
37
38 import net.pterodactylus.sone.core.FreenetInterface.Fetched;
39 import net.pterodactylus.sone.data.Album;
40 import net.pterodactylus.sone.data.Album.Modifier;
41 import net.pterodactylus.sone.data.Client;
42 import net.pterodactylus.sone.data.Image;
43 import net.pterodactylus.sone.data.Post;
44 import net.pterodactylus.sone.data.PostReply;
45 import net.pterodactylus.sone.data.Profile;
46 import net.pterodactylus.sone.data.Sone;
47 import net.pterodactylus.sone.data.Sone.SoneStatus;
48 import net.pterodactylus.sone.database.AlbumBuilder;
49 import net.pterodactylus.sone.database.ImageBuilder;
50 import net.pterodactylus.sone.database.PostBuilder;
51 import net.pterodactylus.sone.database.PostReplyBuilder;
52 import net.pterodactylus.sone.freenet.wot.Identity;
53
54 import freenet.client.ClientMetadata;
55 import freenet.client.FetchResult;
56 import freenet.client.async.USKCallback;
57 import freenet.keys.FreenetURI;
58 import freenet.support.api.Bucket;
59
60 import com.google.common.base.Optional;
61 import com.google.common.collect.ArrayListMultimap;
62 import com.google.common.collect.ImmutableSet;
63 import com.google.common.collect.ListMultimap;
64 import org.junit.Before;
65 import org.junit.Test;
66 import org.mockito.ArgumentCaptor;
67 import org.mockito.invocation.InvocationOnMock;
68 import org.mockito.stubbing.Answer;
69
70 /**
71  * Unit test for {@link SoneDownloaderImpl} and its subclasses.
72  *
73  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
74  */
75 public class SoneDownloaderTest {
76
77         private final Core core = mock(Core.class);
78         private final FreenetInterface freenetInterface = mock(FreenetInterface.class);
79         private final SoneDownloaderImpl soneDownloader = new SoneDownloaderImpl(core, freenetInterface);
80         private final FreenetURI requestUri = mock(FreenetURI.class);
81         private final Sone sone = mock(Sone.class);
82         private final PostBuilder postBuilder = mock(PostBuilder.class);
83         private final List<Post> createdPosts = new ArrayList<Post>();
84         private Post post = mock(Post.class);
85         private final PostReplyBuilder postReplyBuilder = mock(PostReplyBuilder.class);
86         private final Set<PostReply> createdPostReplies = new HashSet<PostReply>();
87         private PostReply postReply = mock(PostReply.class);
88         private final AlbumBuilder albumBuilder = mock(AlbumBuilder.class);
89         private final ListMultimap<Album, Album> nestedAlbums = ArrayListMultimap.create();
90         private final ListMultimap<Album, Image> albumImages = ArrayListMultimap.create();
91         private Album album = mock(Album.class);
92         private final Map<String, Album> albums = new HashMap<String, Album>();
93         private final ImageBuilder imageBuilder = mock(ImageBuilder.class);
94         private Image image = mock(Image.class);
95         private final Map<String, Image> images = new HashMap<String, Image>();
96
97         @Before
98         public void setupSone() {
99                 Identity identity = mock(Identity.class);
100                 when(identity.getId()).thenReturn("identity");
101                 when(sone.getId()).thenReturn("identity");
102                 when(sone.getIdentity()).thenReturn(identity);
103                 when(sone.getRequestUri()).thenReturn(requestUri);
104                 when(sone.getTime()).thenReturn(currentTimeMillis() - DAYS.toMillis(1));
105         }
106
107         private void setupSoneAsUnknown() {
108                 when(sone.getTime()).thenReturn(0L);
109         }
110
111         @Before
112         public void setupRequestUri() {
113                 when(requestUri.setKeyType(anyString())).thenReturn(requestUri);
114                 when(requestUri.sskForUSK()).thenReturn(requestUri);
115                 when(requestUri.setDocName(anyString())).thenReturn(requestUri);
116                 when(requestUri.setMetaString(any(String[].class))).thenReturn(requestUri);
117                 when(requestUri.getKeyType()).thenReturn("USK");
118         }
119
120         @Before
121         public void setupPost() {
122                 when(post.getRecipientId()).thenReturn(Optional.<String>absent());
123         }
124
125         @Before
126         public void setupPostBuilder() {
127                 when(postBuilder.withId(anyString())).thenAnswer(new Answer<PostBuilder>() {
128                         @Override
129                         public PostBuilder answer(InvocationOnMock invocation) throws Throwable {
130                                 when(post.getId()).thenReturn((String) invocation.getArguments()[0]);
131                                 return postBuilder;
132                         }
133                 });
134                 when(postBuilder.from(anyString())).thenAnswer(new Answer<PostBuilder>() {
135                         @Override
136                         public PostBuilder answer(InvocationOnMock invocation) throws Throwable {
137                                 final Sone sone = mock(Sone.class);
138                                 when(sone.getId()).thenReturn((String) invocation.getArguments()[0]);
139                                 when(post.getSone()).thenReturn(sone);
140                                 return postBuilder;
141                         }
142                 });
143                 when(postBuilder.withTime(anyLong())).thenAnswer(new Answer<PostBuilder>() {
144                         @Override
145                         public PostBuilder answer(InvocationOnMock invocation) throws Throwable {
146                                 when(post.getTime()).thenReturn((Long) invocation.getArguments()[0]);
147                                 return postBuilder;
148                         }
149                 });
150                 when(postBuilder.withText(anyString())).thenAnswer(new Answer<PostBuilder>() {
151                         @Override
152                         public PostBuilder answer(InvocationOnMock invocation) throws Throwable {
153                                 when(post.getText()).thenReturn((String) invocation.getArguments()[0]);
154                                 return postBuilder;
155                         }
156                 });
157                 when(postBuilder.to(anyString())).thenAnswer(new Answer<PostBuilder>() {
158                         @Override
159                         public PostBuilder answer(InvocationOnMock invocation) throws Throwable {
160                                 when(post.getRecipientId()).thenReturn(of((String) invocation.getArguments()[0]));
161                                 return postBuilder;
162                         }
163                 });
164                 when(postBuilder.build()).thenAnswer(new Answer<Post>() {
165                         @Override
166                         public Post answer(InvocationOnMock invocation) throws Throwable {
167                                 Post post = SoneDownloaderTest.this.post;
168                                 SoneDownloaderTest.this.post = mock(Post.class);
169                                 setupPost();
170                                 createdPosts.add(post);
171                                 return post;
172                         }
173                 });
174                 when(core.postBuilder()).thenReturn(postBuilder);
175         }
176
177         @Before
178         public void setupPostReplyBuilder() {
179                 when(postReplyBuilder.withId(anyString())).thenAnswer(new Answer<PostReplyBuilder>() {
180                         @Override
181                         public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable {
182                                 when(postReply.getId()).thenReturn((String) invocation.getArguments()[0]);
183                                 return postReplyBuilder;
184                         }
185                 });
186                 when(postReplyBuilder.from(anyString())).thenAnswer(new Answer<PostReplyBuilder>() {
187                         @Override
188                         public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable {
189                                 Sone sone = when(mock(Sone.class).getId()).thenReturn((String) invocation.getArguments()[0]).getMock();
190                                 when(postReply.getSone()).thenReturn(sone);
191                                 return postReplyBuilder;
192                         }
193                 });
194                 when(postReplyBuilder.to(anyString())).thenAnswer(new Answer<PostReplyBuilder>() {
195                         @Override
196                         public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable {
197                                 when(postReply.getPostId()).thenReturn((String) invocation.getArguments()[0]);
198                                 Post post = when(mock(Post.class).getId()).thenReturn((String) invocation.getArguments()[0]).getMock();
199                                 when(postReply.getPost()).thenReturn(of(post));
200                                 return postReplyBuilder;
201                         }
202                 });
203                 when(postReplyBuilder.withTime(anyLong())).thenAnswer(new Answer<PostReplyBuilder>() {
204                         @Override
205                         public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable {
206                                 when(postReply.getTime()).thenReturn((Long) invocation.getArguments()[0]);
207                                 return postReplyBuilder;
208                         }
209                 });
210                 when(postReplyBuilder.withText(anyString())).thenAnswer(new Answer<PostReplyBuilder>() {
211                         @Override
212                         public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable {
213                                 when(postReply.getText()).thenReturn((String) invocation.getArguments()[0]);
214                                 return postReplyBuilder;
215                         }
216                 });
217                 when(postReplyBuilder.build()).thenAnswer(new Answer<PostReply>() {
218                         @Override
219                         public PostReply answer(InvocationOnMock invocation) throws Throwable {
220                                 PostReply postReply = SoneDownloaderTest.this.postReply;
221                                 createdPostReplies.add(postReply);
222                                 SoneDownloaderTest.this.postReply = mock(PostReply.class);
223                                 return postReply;
224                         }
225                 });
226                 when(core.postReplyBuilder()).thenReturn(postReplyBuilder);
227         }
228
229         @Before
230         public void setupAlbum() {
231                 final Album album = SoneDownloaderTest.this.album;
232                 when(album.getAlbumImage()).thenReturn(mock(Image.class));
233                 doAnswer(new Answer<Void>() {
234                         @Override
235                         public Void answer(InvocationOnMock invocation) {
236                                 nestedAlbums.put(album, (Album) invocation.getArguments()[0]);
237                                 return null;
238                         }
239                 }).when(album).addAlbum(any(Album.class));
240                 doAnswer(new Answer<Void>() {
241                         @Override
242                         public Void answer(InvocationOnMock invocation) {
243                                 albumImages.put(album, (Image) invocation.getArguments()[0]);
244                                 return null;
245                         }
246                 }).when(album).addImage(any(Image.class));
247                 when(album.getAlbums()).thenAnswer(new Answer<List<Album>>() {
248                         @Override
249                         public List<Album> answer(InvocationOnMock invocation) {
250                                 return nestedAlbums.get(album);
251                         }
252                 });
253                 when(album.getImages()).thenAnswer(new Answer<List<Image>>() {
254                         @Override
255                         public List<Image> answer(InvocationOnMock invocation) {
256                                 return albumImages.get(album);
257                         }
258                 });
259                 final Modifier albumModifier = new Modifier() {
260                         private String title = album.getTitle();
261                         private String description = album.getDescription();
262                         private String imageId = album.getAlbumImage().getId();
263
264                         @Override
265                         public Modifier setTitle(String title) {
266                                 this.title = title;
267                                 return this;
268                         }
269
270                         @Override
271                         public Modifier setDescription(String description) {
272                                 this.description = description;
273                                 return this;
274                         }
275
276                         @Override
277                         public Modifier setAlbumImage(String imageId) {
278                                 this.imageId = imageId;
279                                 return this;
280                         }
281
282                         @Override
283                         public Album update() throws IllegalStateException {
284                                 when(album.getTitle()).thenReturn(title);
285                                 when(album.getDescription()).thenReturn(description);
286                                 Image image = mock(Image.class);
287                                 when(image.getId()).thenReturn(imageId);
288                                 when(album.getAlbumImage()).thenReturn(image);
289                                 return album;
290                         }
291                 };
292                 when(album.modify()).thenReturn(albumModifier);
293         }
294
295         @Before
296         public void setupAlbumBuilder() {
297                 when(albumBuilder.withId(anyString())).thenAnswer(new Answer<AlbumBuilder>() {
298                         @Override
299                         public AlbumBuilder answer(InvocationOnMock invocation) {
300                                 when(album.getId()).thenReturn((String) invocation.getArguments()[0]);
301                                 return albumBuilder;
302                         }
303                 });
304                 when(albumBuilder.randomId()).thenAnswer(new Answer<AlbumBuilder>() {
305                         @Override
306                         public AlbumBuilder answer(InvocationOnMock invocation) {
307                                 when(album.getId()).thenReturn(randomUUID().toString());
308                                 return albumBuilder;
309                         }
310                 });
311                 when(albumBuilder.by(any(Sone.class))).thenAnswer(new Answer<AlbumBuilder>() {
312                         @Override
313                         public AlbumBuilder answer(InvocationOnMock invocation) {
314                                 when(album.getSone()).thenReturn((Sone) invocation.getArguments()[0]);
315                                 return albumBuilder;
316                         }
317                 });
318                 when(albumBuilder.build()).thenAnswer(new Answer<Album>() {
319                         @Override
320                         public Album answer(InvocationOnMock invocation) {
321                                 Album album = SoneDownloaderTest.this.album;
322                                 albums.put(album.getId(), album);
323                                 SoneDownloaderTest.this.album = mock(Album.class);
324                                 setupAlbum();
325                                 return album;
326                         }
327                 });
328                 when(core.albumBuilder()).thenReturn(albumBuilder);
329         }
330
331         @Before
332         public void setupAlbums() {
333                 when(core.getAlbum(anyString())).thenAnswer(new Answer<Album>() {
334                         @Override
335                         public Album answer(InvocationOnMock invocation) throws Throwable {
336                                 return albums.get(invocation.getArguments()[0]);
337                         }
338                 });
339         }
340
341         @Before
342         public void setupImage() {
343                 final Image image = SoneDownloaderTest.this.image;
344                 Image.Modifier modifier = new Image.Modifier() {
345                         private Sone sone = image.getSone();
346                         private long creationTime = image.getCreationTime();
347                         private String key = image.getKey();
348                         private String title = image.getTitle();
349                         private String description = image.getDescription();
350                         private int width = image.getWidth();
351                         private int height = image.getHeight();
352
353                         @Override
354                         public Image.Modifier setSone(Sone sone) {
355                                 this.sone = sone;
356                                 return this;
357                         }
358
359                         @Override
360                         public Image.Modifier setCreationTime(long creationTime) {
361                                 this.creationTime = creationTime;
362                                 return this;
363                         }
364
365                         @Override
366                         public Image.Modifier setKey(String key) {
367                                 this.key = key;
368                                 return this;
369                         }
370
371                         @Override
372                         public Image.Modifier setTitle(String title) {
373                                 this.title = title;
374                                 return this;
375                         }
376
377                         @Override
378                         public Image.Modifier setDescription(String description) {
379                                 this.description = description;
380                                 return this;
381                         }
382
383                         @Override
384                         public Image.Modifier setWidth(int width) {
385                                 this.width = width;
386                                 return this;
387                         }
388
389                         @Override
390                         public Image.Modifier setHeight(int height) {
391                                 this.height = height;
392                                 return this;
393                         }
394
395                         @Override
396                         public Image update() throws IllegalStateException {
397                                 when(image.getSone()).thenReturn(sone);
398                                 when(image.getCreationTime()).thenReturn(creationTime);
399                                 when(image.getKey()).thenReturn(key);
400                                 when(image.getTitle()).thenReturn(title);
401                                 when(image.getDescription()).thenReturn(description);
402                                 when(image.getWidth()).thenReturn(width);
403                                 when(image.getHeight()).thenReturn(height);
404                                 return image;
405                         }
406                 };
407                 when(image.getSone()).thenReturn(sone);
408                 when(image.modify()).thenReturn(modifier);
409         }
410
411         @Before
412         public void setupImageBuilder() {
413                 when(imageBuilder.randomId()).thenAnswer(new Answer<ImageBuilder>() {
414                         @Override
415                         public ImageBuilder answer(InvocationOnMock invocation) {
416                                 when(image.getId()).thenReturn(randomUUID().toString());
417                                 return imageBuilder;
418                         }
419                 });
420                 when(imageBuilder.withId(anyString())).thenAnswer(new Answer<ImageBuilder>() {
421                         @Override
422                         public ImageBuilder answer(InvocationOnMock invocation) {
423                                 when(image.getId()).thenReturn(
424                                                 (String) invocation.getArguments()[0]);
425                                 return imageBuilder;
426                         }
427                 });
428                 when(imageBuilder.build()).thenAnswer(new Answer<Image>() {
429                         @Override
430                         public Image answer(InvocationOnMock invocation) {
431                                 Image image = SoneDownloaderTest.this.image;
432                                 images.put(image.getId(), image);
433                                 SoneDownloaderTest.this.image = mock(Image.class);
434                                 setupImage();
435                                 return image;
436                         }
437                 });
438                 when(core.imageBuilder()).thenReturn(imageBuilder);
439         }
440
441         @Before
442         public void setupImages() {
443                 when(core.getImage(anyString())).thenAnswer(new Answer<Image>() {
444                         @Override
445                         public Image answer(InvocationOnMock invocation)
446                         throws Throwable {
447                                 return images.get(invocation.getArguments()[0]);
448                         }
449                 });
450         }
451
452         @Test
453         public void addingASoneWillRegisterItsKey() {
454                 soneDownloader.addSone(sone);
455                 verify(freenetInterface).registerActiveUsk(eq(sone.getRequestUri()), any(
456                                 USKCallback.class));
457                 verify(freenetInterface, never()).unregisterUsk(sone);
458         }
459
460         @Test
461         public void addingASoneTwiceWillAlsoDeregisterItsKey() {
462                 soneDownloader.addSone(sone);
463                 soneDownloader.addSone(sone);
464                 verify(freenetInterface, times(2)).registerActiveUsk(eq(
465                                 sone.getRequestUri()), any(USKCallback.class));
466                 verify(freenetInterface).unregisterUsk(sone);
467         }
468
469         @Test
470         public void parsingASoneFailsWhenDocumentIsNotXml() throws SoneException {
471                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-not-xml.xml");
472                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
473         }
474
475         @Test
476         public void parsingASoneFailsWhenDocumentHasNegativeProtocolVersion() throws SoneException {
477                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-negative-protocol-version.xml");
478                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
479         }
480
481         @Test
482         public void parsingASoneFailsWhenProtocolVersionIsTooLarge() throws SoneException {
483                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-too-large-protocol-version.xml");
484                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
485         }
486
487         @Test
488         public void parsingASoneFailsWhenThereIsNoTime() throws SoneException {
489                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-time.xml");
490                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
491         }
492
493         @Test
494         public void parsingASoneFailsWhenTimeIsNotNumeric() throws SoneException {
495                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-time-not-numeric.xml");
496                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
497         }
498
499         @Test
500         public void parsingASoneFailsWhenProfileIsMissing() throws SoneException {
501                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-profile.xml");
502                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
503         }
504
505         @Test
506         public void parsingASoneFailsWhenProfileFieldIsMissingAFieldName() throws SoneException {
507                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-missing-field-name.xml");
508                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
509         }
510
511         @Test
512         public void parsingASoneFailsWhenProfileFieldNameIsEmpty() throws SoneException {
513                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-empty-field-name.xml");
514                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
515         }
516
517         @Test
518         public void parsingASoneFailsWhenProfileFieldNameIsNotUnique() throws SoneException {
519                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-duplicate-field-name.xml");
520                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
521         }
522
523         @Test
524         public void parsingASoneSucceedsWithoutPayload() throws SoneException {
525                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-payload.xml");
526                 assertThat(soneDownloader.parseSone(sone, inputStream).getTime(), is(1407197508000L));
527         }
528
529         @Test
530         public void parsingASoneSucceedsWithoutProtocolVersion() throws SoneException {
531                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-protocol-version.xml");
532                 assertThat(soneDownloader.parseSone(sone, inputStream), not(nullValue()));
533         }
534
535         @Test
536         public void parsingASoneFailsWithMissingClientName() throws SoneException {
537                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-client-name.xml");
538                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
539         }
540
541         @Test
542         public void parsingASoneFailsWithMissingClientVersion() throws SoneException {
543                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-client-version.xml");
544                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
545         }
546
547         @Test
548         public void parsingASoneSucceedsWithClientInfo() throws SoneException {
549                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-client-info.xml");
550                 assertThat(soneDownloader.parseSone(sone, inputStream).getClient(), is(new Client("some-client", "some-version")));
551         }
552
553         @Test
554         public void parsingASoneSucceedsWithRequestUri() throws SoneException, MalformedURLException {
555                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-request-uri.xml");
556                 assertThat(soneDownloader.parseSone(sone, inputStream).getRequestUri().toString(), is("USK@w~RyTGmv12Lg9oO91q1Untupi7my9qczT1RheGkEkVE,E8ElVfUgukSCPHxIEJp-gHMiR80wpM7sID3Jo5O7w1s,AQACAAE/Sone/0"));
557         }
558
559         @Test
560         public void parsingASoneFailsWithInvalidRequestUri() throws SoneException, MalformedURLException {
561                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-request-uri.xml");
562                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
563         }
564
565         @Test
566         public void soneInsertUriIsCopiedToNewSone() throws SoneException {
567                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-payload.xml");
568                 FreenetURI insertUri = mock(FreenetURI.class);
569                 when(insertUri.setKeyType(anyString())).thenReturn(insertUri);
570                 when(insertUri.setDocName(anyString())).thenReturn(insertUri);
571                 when(insertUri.setMetaString(any(String[].class))).thenReturn(insertUri);
572                 when(insertUri.setSuggestedEdition(anyLong())).thenReturn(insertUri);
573                 when(sone.getInsertUri()).thenReturn(insertUri);
574                 assertThat(soneDownloader.parseSone(sone, inputStream).getInsertUri(), is(insertUri));
575         }
576
577         @Test
578         public void parsingASoneSucceedsWithProfile() throws SoneException, MalformedURLException {
579                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-profile.xml");
580                 final Profile profile = soneDownloader.parseSone(sone, inputStream).getProfile();
581                 assertThat(profile.getFirstName(), is("first"));
582                 assertThat(profile.getMiddleName(), is("middle"));
583                 assertThat(profile.getLastName(), is("last"));
584                 assertThat(profile.getBirthDay(), is(18));
585                 assertThat(profile.getBirthMonth(), is(12));
586                 assertThat(profile.getBirthYear(), is(1976));
587         }
588
589         @Test
590         public void parsingASoneSucceedsWithoutProfileFields() throws SoneException, MalformedURLException {
591                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-fields.xml");
592                 assertThat(soneDownloader.parseSone(sone, inputStream), notNullValue());
593         }
594
595         @Test
596         public void parsingASoneFailsWithoutPostId() throws SoneException, MalformedURLException {
597                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-id.xml");
598                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
599         }
600
601         @Test
602         public void parsingASoneFailsWithoutPostTime() throws SoneException, MalformedURLException {
603                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-time.xml");
604                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
605         }
606
607         @Test
608         public void parsingASoneFailsWithoutPostText() throws SoneException, MalformedURLException {
609                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-text.xml");
610                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
611         }
612
613         @Test
614         public void parsingASoneFailsWithInvalidPostTime() throws SoneException, MalformedURLException {
615                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-post-time.xml");
616                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
617         }
618
619         @Test
620         public void parsingASoneSucceedsWithValidPostTime() throws SoneException, MalformedURLException {
621                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-valid-post-time.xml");
622                 final List<Post> posts = soneDownloader.parseSone(sone, inputStream).getPosts();
623                 assertThat(posts, is(createdPosts));
624                 assertThat(posts.get(0).getSone().getId(), is(sone.getId()));
625                 assertThat(posts.get(0).getId(), is("post-id"));
626                 assertThat(posts.get(0).getTime(), is(1407197508000L));
627                 assertThat(posts.get(0).getRecipientId(), is(Optional.<String>absent()));
628                 assertThat(posts.get(0).getText(), is("text"));
629         }
630
631         @Test
632         public void parsingASoneSucceedsWithRecipient() throws SoneException, MalformedURLException {
633                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-recipient.xml");
634                 final List<Post> posts = soneDownloader.parseSone(sone, inputStream).getPosts();
635                 assertThat(posts, is(createdPosts));
636                 assertThat(posts.get(0).getSone().getId(), is(sone.getId()));
637                 assertThat(posts.get(0).getId(), is("post-id"));
638                 assertThat(posts.get(0).getTime(), is(1407197508000L));
639                 assertThat(posts.get(0).getRecipientId(), is(of("1234567890123456789012345678901234567890123")));
640                 assertThat(posts.get(0).getText(), is("text"));
641         }
642
643         @Test
644         public void parsingASoneSucceedsWithInvalidRecipient() throws SoneException, MalformedURLException {
645                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-recipient.xml");
646                 final List<Post> posts = soneDownloader.parseSone(sone, inputStream).getPosts();
647                 assertThat(posts, is(createdPosts));
648                 assertThat(posts.get(0).getSone().getId(), is(sone.getId()));
649                 assertThat(posts.get(0).getId(), is("post-id"));
650                 assertThat(posts.get(0).getTime(), is(1407197508000L));
651                 assertThat(posts.get(0).getRecipientId(), is(Optional.<String>absent()));
652                 assertThat(posts.get(0).getText(), is("text"));
653         }
654
655         @Test
656         public void parsingASoneFailsWithoutPostReplyId() throws SoneException, MalformedURLException {
657                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-id.xml");
658                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
659         }
660
661         @Test
662         public void parsingASoneFailsWithoutPostReplyPostId() throws SoneException, MalformedURLException {
663                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-post-id.xml");
664                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
665         }
666
667         @Test
668         public void parsingASoneFailsWithoutPostReplyTime() throws SoneException, MalformedURLException {
669                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-time.xml");
670                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
671         }
672
673         @Test
674         public void parsingASoneFailsWithoutPostReplyText() throws SoneException, MalformedURLException {
675                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-text.xml");
676                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
677         }
678
679         @Test
680         public void parsingASoneFailsWithInvalidPostReplyTime() throws SoneException, MalformedURLException {
681                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-post-reply-time.xml");
682                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
683         }
684
685         @Test
686         public void parsingASoneSucceedsWithValidPostReplyTime() throws SoneException, MalformedURLException {
687                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-valid-post-reply-time.xml");
688                 final Set<PostReply> postReplies = soneDownloader.parseSone(sone, inputStream).getReplies();
689                 assertThat(postReplies, is(createdPostReplies));
690                 PostReply postReply = createdPostReplies.iterator().next();
691                 assertThat(postReply.getId(), is("reply-id"));
692                 assertThat(postReply.getPostId(), is("post-id"));
693                 assertThat(postReply.getPost().get().getId(), is("post-id"));
694                 assertThat(postReply.getSone().getId(), is("identity"));
695                 assertThat(postReply.getTime(), is(1407197508000L));
696                 assertThat(postReply.getText(), is("reply-text"));
697         }
698
699         @Test
700         public void parsingASoneSucceedsWithoutLikedPostIds() throws SoneException, MalformedURLException {
701                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-liked-post-ids.xml");
702                 assertThat(soneDownloader.parseSone(sone, inputStream), not(nullValue()));
703         }
704
705         @Test
706         public void parsingASoneSucceedsWithLikedPostIds() throws SoneException, MalformedURLException {
707                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-liked-post-ids.xml");
708                 assertThat(soneDownloader.parseSone(sone, inputStream).getLikedPostIds(), is((Set<String>) ImmutableSet.of("liked-post-id")));
709         }
710
711         @Test
712         public void parsingASoneSucceedsWithoutLikedPostReplyIds() throws SoneException, MalformedURLException {
713                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-liked-post-reply-ids.xml");
714                 assertThat(soneDownloader.parseSone(sone, inputStream), not(nullValue()));
715         }
716
717         @Test
718         public void parsingASoneSucceedsWithLikedPostReplyIds() throws SoneException, MalformedURLException {
719                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-liked-post-reply-ids.xml");
720                 assertThat(soneDownloader.parseSone(sone, inputStream).getLikedReplyIds(), is((Set<String>) ImmutableSet.of("liked-post-reply-id")));
721         }
722
723         @Test
724         public void parsingASoneSucceedsWithoutAlbums() throws SoneException, MalformedURLException {
725                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-albums.xml");
726                 assertThat(soneDownloader.parseSone(sone, inputStream), not(nullValue()));
727         }
728
729         @Test
730         public void parsingASoneFailsWithoutAlbumId() throws SoneException, MalformedURLException {
731                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-album-id.xml");
732                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
733         }
734
735         @Test
736         public void parsingASoneFailsWithoutAlbumTitle() throws SoneException, MalformedURLException {
737                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-album-title.xml");
738                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
739         }
740
741         @Test
742         public void parsingASoneSucceedsWithNestedAlbums() throws SoneException, MalformedURLException {
743                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-multiple-albums.xml");
744                 final Sone parsedSone = soneDownloader.parseSone(sone, inputStream);
745                 assertThat(parsedSone, not(nullValue()));
746                 assertThat(parsedSone.getRootAlbum().getAlbums(), hasSize(1));
747                 Album album = parsedSone.getRootAlbum().getAlbums().get(0);
748                 assertThat(album.getId(), is("album-id-1"));
749                 assertThat(album.getTitle(), is("album-title"));
750                 assertThat(album.getDescription(), is("album-description"));
751                 assertThat(album.getAlbums(), hasSize(1));
752                 Album nestedAlbum = album.getAlbums().get(0);
753                 assertThat(nestedAlbum.getId(), is("album-id-2"));
754                 assertThat(nestedAlbum.getTitle(), is("album-title-2"));
755                 assertThat(nestedAlbum.getDescription(), is("album-description-2"));
756                 assertThat(nestedAlbum.getAlbums(), hasSize(0));
757         }
758
759         @Test
760         public void parsingASoneFailsWithInvalidParentAlbumId() throws SoneException, MalformedURLException {
761                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-parent-album-id.xml");
762                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
763         }
764
765         @Test
766         public void parsingASoneSucceedsWithoutImages() throws SoneException, MalformedURLException {
767                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-images.xml");
768                 assertThat(soneDownloader.parseSone(sone, inputStream), not(nullValue()));
769         }
770
771         @Test
772         public void parsingASoneFailsWithoutImageId() throws SoneException, MalformedURLException {
773                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-id.xml");
774                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
775         }
776
777         @Test
778         public void parsingASoneFailsWithoutImageTime() throws SoneException, MalformedURLException {
779                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-time.xml");
780                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
781         }
782
783         @Test
784         public void parsingASoneFailsWithoutImageKey() throws SoneException, MalformedURLException {
785                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-key.xml");
786                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
787         }
788
789         @Test
790         public void parsingASoneFailsWithoutImageTitle() throws SoneException, MalformedURLException {
791                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-title.xml");
792                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
793         }
794
795         @Test
796         public void parsingASoneFailsWithoutImageWidth() throws SoneException, MalformedURLException {
797                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-width.xml");
798                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
799         }
800
801         @Test
802         public void parsingASoneFailsWithoutImageHeight() throws SoneException, MalformedURLException {
803                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-height.xml");
804                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
805         }
806
807         @Test
808         public void parsingASoneFailsWithInvalidImageWidth() throws SoneException, MalformedURLException {
809                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-image-width.xml");
810                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
811         }
812
813         @Test
814         public void parsingASoneFailsWithInvalidImageHeight() throws SoneException, MalformedURLException {
815                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-image-height.xml");
816                 assertThat(soneDownloader.parseSone(sone, inputStream), nullValue());
817         }
818
819         @Test
820         public void parsingASoneSucceedsWithImage() throws SoneException, MalformedURLException {
821                 InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-image.xml");
822                 final Sone sone = soneDownloader.parseSone(this.sone, inputStream);
823                 assertThat(sone, not(nullValue()));
824                 assertThat(sone.getRootAlbum().getAlbums(), hasSize(1));
825                 assertThat(sone.getRootAlbum().getAlbums().get(0).getImages(), hasSize(1));
826                 Image image = sone.getRootAlbum().getAlbums().get(0).getImages().get(0);
827                 assertThat(image.getId(), is("image-id"));
828                 assertThat(image.getCreationTime(), is(1407197508000L));
829                 assertThat(image.getKey(), is("KSK@GPLv3.txt"));
830                 assertThat(image.getTitle(), is("image-title"));
831                 assertThat(image.getDescription(), is("image-description"));
832                 assertThat(image.getWidth(), is(1920));
833                 assertThat(image.getHeight(), is(1080));
834                 assertThat(sone.getProfile().getAvatar(), is("image-id"));
835         }
836
837         @Test
838         public void stoppingTheSoneDownloaderUnregistersTheSone() {
839                 soneDownloader.addSone(sone);
840                 soneDownloader.stop();
841                 verify(freenetInterface).unregisterUsk(sone);
842         }
843
844         @Test
845         public void notBeingAbleToFetchAnUnknownSoneDoesNotUpdateCore() {
846                 setupSoneAsUnknown();
847                 soneDownloader.fetchSoneAction(sone).run();
848                 verify(freenetInterface).fetchUri(requestUri);
849                 verifyThatSoneStatusWasChangedToDownloadingAndBackTo(unknown);
850                 verify(core, never()).updateSone(any(Sone.class));
851         }
852
853         private void verifyThatSoneStatusWasChangedToDownloadingAndBackTo(SoneStatus soneStatus) {
854                 ArgumentCaptor<SoneStatus> soneStatuses = forClass(SoneStatus.class);
855                 verify(sone, times(2)).setStatus(soneStatuses.capture());
856                 assertThat(soneStatuses.getAllValues().get(0), is(downloading));
857                 assertThat(soneStatuses.getAllValues().get(1), is(soneStatus));
858         }
859
860         @Test
861         public void notBeingAbleToFetchAKnownSoneDoesNotUpdateCore() {
862                 soneDownloader.fetchSoneAction(sone).run();
863                 verify(freenetInterface).fetchUri(requestUri);
864                 verifyThatSoneStatusWasChangedToDownloadingAndBackTo(idle);
865                 verify(core, never()).updateSone(any(Sone.class));
866         }
867
868         @Test(expected = NullPointerException.class)
869         public void exceptionWhileFetchingAnUnknownSoneDoesNotUpdateCore() {
870                 setupSoneAsUnknown();
871                 when(freenetInterface.fetchUri(requestUri)).thenThrow(NullPointerException.class);
872                 try {
873                         soneDownloader.fetchSoneAction(sone).run();
874                 } finally {
875                         verify(freenetInterface).fetchUri(requestUri);
876                         verifyThatSoneStatusWasChangedToDownloadingAndBackTo(unknown);
877                         verify(core, never()).updateSone(any(Sone.class));
878                 }
879         }
880
881         @Test(expected = NullPointerException.class)
882         public void exceptionWhileFetchingAKnownSoneDoesNotUpdateCore() {
883                 when(freenetInterface.fetchUri(requestUri)).thenThrow(NullPointerException.class);
884                 try {
885                         soneDownloader.fetchSoneAction(sone).run();
886                 } finally {
887                         verify(freenetInterface).fetchUri(requestUri);
888                         verifyThatSoneStatusWasChangedToDownloadingAndBackTo(idle);
889                         verify(core, never()).updateSone(any(Sone.class));
890                 }
891         }
892
893         @Test
894         public void successfulFetchingOfSoneWithUskRequestUriUpdatesTheCoreWithASone() throws IOException {
895                 final Fetched fetchResult = createFetchResult(requestUri, getClass().getResourceAsStream("sone-parser-no-payload.xml"));
896                 when(freenetInterface.fetchUri(requestUri)).thenReturn(fetchResult);
897                 soneDownloader.fetchSoneAction(sone).run();
898                 verifyThatParsedSoneHasTheSameIdAsTheOriginalSone();
899         }
900
901         private void verifyThatParsedSoneHasTheSameIdAsTheOriginalSone() {
902                 ArgumentCaptor<Sone> soneCaptor = forClass(Sone.class);
903                 verify(core).updateSone(soneCaptor.capture());
904                 assertThat(soneCaptor.getValue().getId(), is(sone.getId()));
905         }
906
907         @Test
908         public void successfulFetchingOfSoneWithSskRequestUriUpdatesTheCoreWithASone() throws IOException {
909                 when(requestUri.getKeyType()).thenReturn("SSK");
910                 final Fetched fetchResult = createFetchResult(requestUri, getClass().getResourceAsStream("sone-parser-no-payload.xml"));
911                 when(freenetInterface.fetchUri(requestUri)).thenReturn(fetchResult);
912                 soneDownloader.fetchSoneAction(sone).run();
913                 verifyThatParsedSoneHasTheSameIdAsTheOriginalSone();
914         }
915
916         @Test
917         public void successfulFetchingAnUnknownSoneWithSskRequestUriUpdatesTheCoreWithASone() throws IOException {
918                 when(requestUri.getKeyType()).thenReturn("SSK");
919                 final Fetched fetchResult = createFetchResult(requestUri, getClass().getResourceAsStream("sone-parser-with-zero-time.xml"));
920                 when(freenetInterface.fetchUri(requestUri)).thenReturn(fetchResult);
921                 soneDownloader.fetchSoneAction(sone).run();
922                 verifyThatParsedSoneHasTheSameIdAsTheOriginalSone();
923         }
924
925         @Test
926         public void fetchingSoneWithInvalidXmlWillNotUpdateTheCore() throws IOException {
927                 final Fetched fetchResult = createFetchResult(requestUri, getClass().getResourceAsStream("sone-parser-not-xml.xml"));
928                 when(freenetInterface.fetchUri(requestUri)).thenReturn(fetchResult);
929                 soneDownloader.fetchSoneAction(sone).run();
930                 verify(core, never()).updateSone(any(Sone.class));
931         }
932
933         @Test
934         public void exceptionWhileFetchingSoneWillNotUpdateTheCore() throws IOException {
935                 final Fetched fetchResult = createFetchResult(requestUri, getClass().getResourceAsStream("sone-parser-no-payload.xml"));
936                 when(sone.getId()).thenThrow(NullPointerException.class);
937                 when(freenetInterface.fetchUri(requestUri)).thenReturn(fetchResult);
938                 soneDownloader.fetchSoneAction(sone).run();
939                 verify(core, never()).updateSone(any(Sone.class));
940         }
941
942         @Test
943         public void onlyFetchingASoneWillNotUpdateTheCore() throws IOException {
944                 final Fetched fetchResult = createFetchResult(requestUri, getClass().getResourceAsStream("sone-parser-no-payload.xml"));
945                 when(freenetInterface.fetchUri(requestUri)).thenReturn(fetchResult);
946                 soneDownloader.fetchSone(sone, sone.getRequestUri(), true);
947                 verify(core, never()).updateSone(any(Sone.class));
948                 verifyThatSoneStatusWasChangedToDownloadingAndBackTo(idle);
949         }
950
951         private Fetched createFetchResult(FreenetURI uri, InputStream inputStream) throws IOException {
952                 ClientMetadata clientMetadata = new ClientMetadata("application/xml");
953                 Bucket bucket = mock(Bucket.class);
954                 when(bucket.getInputStream()).thenReturn(inputStream);
955                 FetchResult fetchResult = new FetchResult(clientMetadata, bucket);
956                 return new Fetched(uri, fetchResult);
957         }
958
959 }