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