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