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