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