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