Merge branch '1822-rss-feeds-backend' into 'master'

Resolve "Import RSS feeds shared by other apps"

See merge request briar/briar!1763
This commit is contained in:
Torsten Grote
2023-01-25 11:39:34 +00:00
24 changed files with 1176 additions and 291 deletions

View File

@@ -0,0 +1,159 @@
package org.briarproject.briar.feed;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.identity.AuthorFactory;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogFactory;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.RssProperties;
import org.jmock.Expectations;
import org.junit.Test;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
import static org.briarproject.briar.api.blog.BlogManager.MAJOR_VERSION;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_ADDED;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_AUTHOR;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_DESC;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_LAST_ENTRY;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_PRIVATE_KEY;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_RSS_AUTHOR;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_RSS_LINK;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_RSS_TITLE;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_RSS_URI;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_UPDATED;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEED_URL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class FeedFactoryImplTest extends BrambleMockTestCase {
private final AuthorFactory authorFactory =
context.mock(AuthorFactory.class);
private final BlogFactory blogFactory = context.mock(BlogFactory.class);
private final ClientHelper clientHelper = context.mock(ClientHelper.class);
private final Clock clock = context.mock(Clock.class);
private final LocalAuthor localAuthor = getLocalAuthor();
private final Group blogGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
private final Blog blog = new Blog(blogGroup, localAuthor, true);
private final BdfList authorList = BdfList.of("foo");
private final long added = 123, updated = 234, lastEntryTime = 345;
private final String url = getRandomString(123);
private final String description = getRandomString(123);
private final String rssAuthor = getRandomString(123);
private final String title = getRandomString(123);
private final String link = getRandomString(123);
private final String uri = getRandomString(123);
private final FeedFactoryImpl feedFactory = new FeedFactoryImpl(
authorFactory, blogFactory, clientHelper, clock);
@Test
public void testSerialiseAndDeserialiseWithoutOptionalFields()
throws Exception {
RssProperties propertiesBefore = new RssProperties(null, null, null,
null, null, null);
Feed before = new Feed(blog, localAuthor, propertiesBefore, added,
updated, lastEntryTime);
context.checking(new Expectations() {{
oneOf(clientHelper).toList(localAuthor);
will(returnValue(authorList));
}});
BdfDictionary dict = feedFactory.feedToBdfDictionary(before);
BdfDictionary expectedDict = BdfDictionary.of(
new BdfEntry(KEY_FEED_AUTHOR, authorList),
new BdfEntry(KEY_FEED_PRIVATE_KEY, localAuthor.getPrivateKey()),
new BdfEntry(KEY_FEED_ADDED, added),
new BdfEntry(KEY_FEED_UPDATED, updated),
new BdfEntry(KEY_FEED_LAST_ENTRY, lastEntryTime)
);
assertEquals(expectedDict, dict);
context.checking(new Expectations() {{
oneOf(clientHelper).parseAndValidateAuthor(authorList);
will(returnValue(localAuthor));
oneOf(blogFactory).createFeedBlog(localAuthor);
will(returnValue(blog));
}});
Feed after = feedFactory.createFeed(dict);
RssProperties afterProperties = after.getProperties();
assertNull(afterProperties.getUrl());
assertNull(afterProperties.getTitle());
assertNull(afterProperties.getDescription());
assertNull(afterProperties.getAuthor());
assertNull(afterProperties.getLink());
assertNull(afterProperties.getUri());
assertEquals(added, after.getAdded());
assertEquals(updated, after.getUpdated());
assertEquals(lastEntryTime, after.getLastEntryTime());
}
@Test
public void testSerialiseAndDeserialiseWithOptionalFields()
throws Exception {
RssProperties propertiesBefore = new RssProperties(url, title,
description, rssAuthor, link, uri);
Feed before = new Feed(blog, localAuthor, propertiesBefore, added,
updated, lastEntryTime);
context.checking(new Expectations() {{
oneOf(clientHelper).toList(localAuthor);
will(returnValue(authorList));
}});
BdfDictionary dict = feedFactory.feedToBdfDictionary(before);
BdfDictionary expectedDict = BdfDictionary.of(
new BdfEntry(KEY_FEED_AUTHOR, authorList),
new BdfEntry(KEY_FEED_PRIVATE_KEY, localAuthor.getPrivateKey()),
new BdfEntry(KEY_FEED_ADDED, added),
new BdfEntry(KEY_FEED_UPDATED, updated),
new BdfEntry(KEY_FEED_LAST_ENTRY, lastEntryTime),
new BdfEntry(KEY_FEED_URL, url),
new BdfEntry(KEY_FEED_RSS_TITLE, title),
new BdfEntry(KEY_FEED_DESC, description),
new BdfEntry(KEY_FEED_RSS_AUTHOR, rssAuthor),
new BdfEntry(KEY_FEED_RSS_LINK, link),
new BdfEntry(KEY_FEED_RSS_URI, uri)
);
assertEquals(expectedDict, dict);
context.checking(new Expectations() {{
oneOf(clientHelper).parseAndValidateAuthor(authorList);
will(returnValue(localAuthor));
oneOf(blogFactory).createFeedBlog(localAuthor);
will(returnValue(blog));
}});
Feed after = feedFactory.createFeed(dict);
RssProperties afterProperties = after.getProperties();
assertEquals(url, afterProperties.getUrl());
assertEquals(title, afterProperties.getTitle());
assertEquals(description, afterProperties.getDescription());
assertEquals(rssAuthor, afterProperties.getAuthor());
assertEquals(link, afterProperties.getLink());
assertEquals(uri, afterProperties.getUri());
assertEquals(added, after.getAdded());
assertEquals(updated, after.getUpdated());
assertEquals(lastEntryTime, after.getLastEntryTime());
}
}

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.feed;
import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.SyndEntryImpl;
import com.rometools.rome.feed.synd.SyndFeed;
import org.briarproject.bramble.api.WeakSingletonProvider;
import org.briarproject.bramble.api.client.ClientHelper;
@@ -25,28 +24,31 @@ import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPost;
import org.briarproject.briar.api.blog.BlogPostFactory;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.RssProperties;
import org.jmock.Expectations;
import org.junit.Test;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.io.ByteArrayInputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executor;
import javax.annotation.Nonnull;
import javax.net.SocketFactory;
import okhttp3.Dns;
import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.Collections.singletonList;
import static okhttp3.mockwebserver.SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.util.StringUtils.UTF_8;
import static org.briarproject.briar.api.feed.FeedConstants.KEY_FEEDS;
import static org.briarproject.briar.api.feed.FeedManager.CLIENT_ID;
import static org.briarproject.briar.api.feed.FeedManager.MAJOR_VERSION;
import static org.hamcrest.Matchers.nullValue;
public class FeedManagerImplTest extends BrambleMockTestCase {
@@ -60,14 +62,10 @@ public class FeedManagerImplTest extends BrambleMockTestCase {
private final BlogPostFactory blogPostFactory =
context.mock(BlogPostFactory.class);
private final FeedFactory feedFactory = context.mock(FeedFactory.class);
private final FeedMatcher feedMatcher = context.mock(FeedMatcher.class);
private final Clock clock = context.mock(Clock.class);
private final Dns noDnsLookups = context.mock(Dns.class);
private final OkHttpClient client = new OkHttpClient.Builder()
.socketFactory(SocketFactory.getDefault())
.dns(noDnsLookups)
.connectTimeout(60_000, MILLISECONDS)
.build();
private final OkHttpClient client = new OkHttpClient.Builder().build();
private final WeakSingletonProvider<OkHttpClient> httpClientProvider =
new WeakSingletonProvider<OkHttpClient>() {
@Override
@@ -83,14 +81,20 @@ public class FeedManagerImplTest extends BrambleMockTestCase {
private final GroupId blogGroupId = blogGroup.getId();
private final LocalAuthor localAuthor = getLocalAuthor();
private final Blog blog = new Blog(blogGroup, localAuthor, true);
private final Feed feed =
new Feed("http://example.org", blog, localAuthor, 0);
private final BdfDictionary feedDict = new BdfDictionary();
private final Message message = getMessage(blogGroupId);
private final BlogPost blogPost = new BlogPost(message, null, localAuthor);
private final long now = System.currentTimeMillis();
// Round the publication date to a whole second to avoid rounding errors
private final long pubDate = now / 1000 * 1000 - 1000;
private final SimpleDateFormat sdf =
new SimpleDateFormat("EEE, dd MMM yy HH:mm:ss Z");
private final String pubDateString = sdf.format(new Date(pubDate));
private final FeedManagerImpl feedManager =
new FeedManagerImpl(scheduler, ioExecutor, db, contactGroupFactory,
clientHelper, blogManager, blogPostFactory, feedFactory,
httpClientProvider, clock);
feedMatcher, httpClientProvider, clock);
@Test
public void testFetchFeedsReturnsEarlyIfTorIsNotActive() {
@@ -99,55 +103,223 @@ public class FeedManagerImplTest extends BrambleMockTestCase {
}
@Test
public void testEmptyFetchFeeds() throws Exception {
BdfList feedList = new BdfList();
expectGetFeeds(feedList);
expectStoreFeed(feedList);
public void testFetchFeedsEmptyList() throws Exception {
// The list of feeds is empty
expectGetFeeds();
feedManager.setTorActive(true);
feedManager.fetchFeeds();
}
@Test
public void testFetchFeedsIoException() throws Exception {
BdfDictionary feedDict = new BdfDictionary();
BdfList feedList = BdfList.of(feedDict);
// Fetching the feed will fail
MockWebServer server = new MockWebServer();
String url = server.url("/").toString();
server.enqueue(new MockResponse()
.setBody(" ")
.setSocketPolicy(DISCONNECT_DURING_RESPONSE_BODY));
expectGetFeeds(feedList);
context.checking(new Expectations() {{
oneOf(noDnsLookups).lookup("example.org");
will(throwException(new UnknownHostException()));
}});
expectStoreFeed(feedList);
Feed feed = createFeed(url, blog);
expectGetFeeds(feed);
expectGetAndStoreFeeds(feed);
feedManager.setTorActive(true);
feedManager.fetchFeeds();
}
@Test
public void testPostFeedEntriesEmptyDate() throws Exception {
Transaction txn = new Transaction(null, false);
List<SyndEntry> entries = new ArrayList<>();
entries.add(new SyndEntryImpl());
SyndEntry entry = new SyndEntryImpl();
entry.setUpdatedDate(new Date());
entries.add(entry);
String text = "<p>(" + entry.getUpdatedDate().toString() + ")</p>";
Message msg = getMessage(blogGroupId);
BlogPost post = new BlogPost(msg, null, localAuthor);
public void testFetchFeedsEmptyResponseBody() throws Exception {
// Fetching the feed will succeed, but parsing the empty body will fail
MockWebServer server = new MockWebServer();
String url = server.url("/").toString();
server.enqueue(new MockResponse());
context.checking(new Expectations() {{
oneOf(db).startTransaction(false);
will(returnValue(txn));
oneOf(clock).currentTimeMillis();
will(returnValue(42L));
oneOf(blogPostFactory).createBlogPost(feed.getBlogId(), 42L, null,
localAuthor, text);
will(returnValue(post));
oneOf(blogManager).addLocalPost(txn, post);
oneOf(db).commitTransaction(txn);
oneOf(db).endTransaction(txn);
Feed feed = createFeed(url, blog);
expectGetFeeds(feed);
expectGetAndStoreFeeds(feed);
feedManager.setTorActive(true);
feedManager.fetchFeeds();
}
@Test
public void testFetchFeedsNoEntries() throws Exception {
// Fetching and parsing the feed will succeed; there are no entries
String feedXml = createRssFeedXml();
MockWebServer server = new MockWebServer();
String url = server.url("/").toString();
server.enqueue(new MockResponse().setBody(feedXml));
Feed feed = createFeed(url, blog);
expectGetFeeds(feed);
expectUpdateFeedNoEntries(feed);
expectGetAndStoreFeeds(feed);
feedManager.setTorActive(true);
feedManager.fetchFeeds();
}
@Test
public void testFetchFeedsOneEntry() throws Exception {
// Fetching and parsing the feed will succeed; there is one entry
String entryXml =
"<item><pubDate>" + pubDateString + "</pubDate></item>";
String feedXml = createRssFeedXml(entryXml);
MockWebServer server = new MockWebServer();
String url = server.url("/").toString();
server.enqueue(new MockResponse().setBody(feedXml));
Feed feed = createFeed(url, blog);
expectGetFeeds(feed);
expectUpdateFeedOneEntry(feed);
expectGetAndStoreFeeds(feed);
feedManager.setTorActive(true);
feedManager.fetchFeeds();
}
@Test
public void testAddNewFeedFromUrl() throws Exception {
// Fetching and parsing the feed will succeed; there are no entries
String feedXml = createRssFeedXml();
MockWebServer server = new MockWebServer();
String url = server.url("/").toString();
server.enqueue(new MockResponse().setBody(feedXml));
Feed newFeed = createFeed(url, blog);
Group existingBlogGroup = getGroup(BlogManager.CLIENT_ID,
BlogManager.MAJOR_VERSION);
Blog existingBlog = new Blog(existingBlogGroup, localAuthor, true);
Feed existingFeed = createFeed("http://example.com", existingBlog);
expectGetFeeds(existingFeed);
context.checking(new DbExpectations() {{
// The added feed doesn't match any existing feed
oneOf(feedMatcher).findMatchingFeed(with(any(RssProperties.class)),
with(singletonList(existingFeed)));
will(returnValue(null));
// Create the new feed
oneOf(feedFactory).createFeed(with(url), with(any(SyndFeed.class)));
will(returnValue(newFeed));
// Add the new feed to the list of feeds
Transaction txn = new Transaction(null, false);
oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(blogManager).addBlog(txn, blog);
expectGetFeeds(txn, existingFeed);
expectStoreFeeds(txn, existingFeed, newFeed);
}});
feedManager.postFeedEntries(feed, entries);
expectUpdateFeedNoEntries(newFeed);
expectGetAndStoreFeeds(existingFeed, newFeed);
feedManager.addFeed(url);
}
@Test
public void testAddExistingFeedFromUrl() throws Exception {
// Fetching and parsing the feed will succeed; there are no entries
String feedXml = createRssFeedXml();
MockWebServer server = new MockWebServer();
String url = server.url("/").toString();
server.enqueue(new MockResponse().setBody(feedXml));
Feed newFeed = createFeed(url, blog);
expectGetFeeds(newFeed);
context.checking(new DbExpectations() {{
// The added feed matches an existing feed
oneOf(feedMatcher).findMatchingFeed(with(any(RssProperties.class)),
with(singletonList(newFeed)));
will(returnValue(newFeed));
}});
expectUpdateFeedNoEntries(newFeed);
expectGetAndStoreFeeds(newFeed);
feedManager.addFeed(url);
}
@Test
public void testAddNewFeedFromInputStream() throws Exception {
// Reading and parsing the feed will succeed; there are no entries
String feedXml = createRssFeedXml();
Feed newFeed = createFeed(null, blog);
Group existingBlogGroup = getGroup(BlogManager.CLIENT_ID,
BlogManager.MAJOR_VERSION);
Blog existingBlog = new Blog(existingBlogGroup, localAuthor, true);
Feed existingFeed = createFeed(null, existingBlog);
expectGetFeeds(existingFeed);
context.checking(new DbExpectations() {{
// The added feed doesn't match any existing feed
oneOf(feedMatcher).findMatchingFeed(with(any(RssProperties.class)),
with(singletonList(existingFeed)));
will(returnValue(null));
// Create the new feed
oneOf(feedFactory).createFeed(with(nullValue(String.class)),
with(any(SyndFeed.class)));
will(returnValue(newFeed));
// Add the new feed to the list of feeds
Transaction txn = new Transaction(null, false);
oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(blogManager).addBlog(txn, blog);
expectGetFeeds(txn, existingFeed);
expectStoreFeeds(txn, existingFeed, newFeed);
}});
expectUpdateFeedNoEntries(newFeed);
expectGetAndStoreFeeds(existingFeed, newFeed);
feedManager.addFeed(new ByteArrayInputStream(feedXml.getBytes(UTF_8)));
}
@Test
public void testAddExistingFeedFromInputStream() throws Exception {
// Reading and parsing the feed will succeed; there are no entries
String feedXml = createRssFeedXml();
Feed newFeed = createFeed(null, blog);
expectGetFeeds(newFeed);
context.checking(new DbExpectations() {{
// The added feed matches an existing feed
oneOf(feedMatcher).findMatchingFeed(with(any(RssProperties.class)),
with(singletonList(newFeed)));
will(returnValue(newFeed));
}});
expectUpdateFeedNoEntries(newFeed);
expectGetAndStoreFeeds(newFeed);
feedManager.addFeed(new ByteArrayInputStream(feedXml.getBytes(UTF_8)));
}
private Feed createFeed(String url, Blog blog) {
RssProperties properties = new RssProperties(url,
null, null, null, null, null);
return new Feed(blog, localAuthor, properties, 0, 0, 0);
}
private String createRssFeedXml(String... entries) {
StringBuilder sb = new StringBuilder();
sb.append("<rss version='2.0'><channel>");
for (String entry : entries) sb.append(entry);
sb.append("</channel></rss>");
return sb.toString();
}
private void expectGetLocalGroup() {
@@ -158,33 +330,88 @@ public class FeedManagerImplTest extends BrambleMockTestCase {
}});
}
private void expectGetFeeds(BdfList feedList) throws Exception {
private void expectGetFeeds(Feed... feeds) throws Exception {
Transaction txn = new Transaction(null, true);
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
}});
expectGetFeeds(txn, feeds);
}
private void expectGetFeeds(Transaction txn, Feed... feeds)
throws Exception {
BdfList feedList = new BdfList();
for (int i = 0; i < feeds.length; i++) {
feedList.add(new BdfDictionary());
}
BdfDictionary feedsDict =
BdfDictionary.of(new BdfEntry(KEY_FEEDS, feedList));
expectGetLocalGroup();
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(true), withDbCallable(txn));
context.checking(new Expectations() {{
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, localGroupId);
will(returnValue(feedsDict));
if (feedList.size() == 1) {
oneOf(feedFactory).createFeed(feedDict);
will(returnValue(feed));
for (int i = 0; i < feeds.length; i++) {
oneOf(feedFactory).createFeed(feedList.getDictionary(i));
will(returnValue(feeds[i]));
}
}});
}
private void expectStoreFeed(BdfList feedList) throws Exception {
private void expectStoreFeeds(Transaction txn, Feed... feeds)
throws Exception {
BdfList feedList = new BdfList();
for (int i = 0; i < feeds.length; i++) {
feedList.add(new BdfDictionary());
}
BdfDictionary feedDict =
BdfDictionary.of(new BdfEntry(KEY_FEEDS, feedList));
expectGetLocalGroup();
context.checking(new Expectations() {{
oneOf(clientHelper).mergeGroupMetadata(localGroupId, feedDict);
if (feedList.size() == 1) {
oneOf(feedFactory).feedToBdfDictionary(feed);
will(returnValue(feedList.getDictionary(0)));
oneOf(clientHelper).mergeGroupMetadata(txn, localGroupId, feedDict);
for (int i = 0; i < feeds.length; i++) {
oneOf(feedFactory).feedToBdfDictionary(feeds[i]);
will(returnValue(feedList.getDictionary(i)));
}
}});
}
private void expectGetAndStoreFeeds(Feed... feeds) throws Exception {
context.checking(new DbExpectations() {{
Transaction txn = new Transaction(null, false);
oneOf(db).transaction(with(false), withDbRunnable(txn));
expectGetFeeds(txn, feeds);
expectStoreFeeds(txn, feeds);
}});
}
private void expectUpdateFeedNoEntries(Feed feed) throws Exception {
Transaction txn = new Transaction(null, false);
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(false), withDbCallable(txn));
oneOf(feedFactory).updateFeed(with(feed), with(any(SyndFeed.class)),
with(0L));
will(returnValue(feed));
}});
}
private void expectUpdateFeedOneEntry(Feed feed) throws Exception {
Transaction txn = new Transaction(null, false);
String body = "<p>(" + new Date(pubDate) + ")</p>";
context.checking(new DbExpectations() {{
oneOf(db).transactionWithResult(with(false), withDbCallable(txn));
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(blogPostFactory).createBlogPost(blogGroupId, pubDate, null,
localAuthor, body);
will(returnValue(blogPost));
oneOf(blogManager).addLocalPost(txn, blogPost);
oneOf(feedFactory).updateFeed(with(feed), with(any(SyndFeed.class)),
with(pubDate));
will(returnValue(feed));
}});
}
}

View File

@@ -5,30 +5,43 @@ import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.TestDatabaseConfigModule;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.FeedManager;
import org.briarproject.nullsafety.NullSafety;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.util.Collection;
import javax.annotation.Nullable;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
import static org.briarproject.nullsafety.NullSafety.requireNonNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class FeedManagerIntegrationTest extends BrambleTestCase {
private static final String FEED_PATH =
"src/test/resources/briarproject.org_news_index.xml";
private static final String FEED_URL =
"https://briarproject.org/news/index.xml";
private static final String FEED_TITLE = "News - Briar";
private LifecycleManager lifecycleManager;
private FeedManager feedManager;
private BlogManager blogManager;
private final File testDir = TestUtils.getTestDirectory();
private final File testDir = getTestDirectory();
private final File testFile = new File(testDir, "feedTest");
@Before
@@ -38,7 +51,8 @@ public class FeedManagerIntegrationTest extends BrambleTestCase {
DaggerFeedManagerIntegrationTestComponent.builder()
.testDatabaseConfigModule(
new TestDatabaseConfigModule(testFile)).build();
FeedManagerIntegrationTestComponent.Helper.injectEagerSingletons(component);
FeedManagerIntegrationTestComponent.Helper
.injectEagerSingletons(component);
component.inject(this);
IdentityManager identityManager = component.getIdentityManager();
@@ -54,17 +68,30 @@ public class FeedManagerIntegrationTest extends BrambleTestCase {
}
@Test
public void testFeedImportAndRemoval() throws Exception {
public void testFeedImportAndRemovalFromUrl() throws Exception {
testFeedImportAndRemoval(FEED_URL, null);
}
@Test
public void testFeedImportAndRemovalFromFile() throws Exception {
testFeedImportAndRemoval(null, FEED_PATH);
}
private void testFeedImportAndRemoval(@Nullable String url,
@Nullable String path) throws Exception {
// initially, there's only the one personal blog
Collection<Blog> blogs = blogManager.getBlogs();
assertEquals(1, blogs.size());
Blog personalBlog = blogs.iterator().next();
// add feed into a dedicated blog
String url = "https://www.schneier.com/blog/atom.xml";
feedManager.addFeed(url);
if (url == null) {
feedManager.addFeed(new FileInputStream(requireNonNull(path)));
} else {
feedManager.addFeed(url);
}
// then there's the feed's blog now
// now there's the feed's blog too
blogs = blogManager.getBlogs();
assertEquals(2, blogs.size());
Blog feedBlog = null;
@@ -80,15 +107,16 @@ public class FeedManagerIntegrationTest extends BrambleTestCase {
assertTrue(feed.getLastEntryTime() > 0);
assertTrue(feed.getAdded() > 0);
assertTrue(feed.getUpdated() > 0);
assertEquals(url, feed.getUrl());
assertTrue(NullSafety.equals(url, feed.getProperties().getUrl()));
assertEquals(feedBlog, feed.getBlog());
assertEquals("Schneier on Security", feed.getTitle());
assertEquals(FEED_TITLE, feed.getTitle());
assertEquals(feed.getTitle(), feed.getBlog().getName());
assertEquals(feed.getTitle(), feed.getLocalAuthor().getName());
// check the feed entries have been added to the blog as expected
Collection<BlogPostHeader> headers =
blogManager.getPostHeaders(feedBlog.getId());
assertFalse(headers.isEmpty());
for (BlogPostHeader header : headers) {
assertTrue(header.isRssFeed());
}
@@ -105,6 +133,6 @@ public class FeedManagerIntegrationTest extends BrambleTestCase {
public void tearDown() throws Exception {
lifecycleManager.stopServices();
lifecycleManager.waitForShutdown();
TestUtils.deleteTestDirectory(testDir);
deleteTestDirectory(testDir);
}
}

View File

@@ -0,0 +1,179 @@
package org.briarproject.briar.feed;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.RssProperties;
import org.junit.Test;
import java.util.Random;
import static java.util.Arrays.asList;
import static org.briarproject.bramble.test.TestUtils.getClientId;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
public class FeedMatcherImplTest extends BrambleTestCase {
private static final String URL = "url";
private static final String TITLE = "title";
private static final String DESCRIPTION = "description";
private static final String AUTHOR = "author";
private static final String LINK = "link";
private static final String URI = "uri";
private final Random random = new Random();
private final ClientId clientId = getClientId();
private final LocalAuthor localAuthor = getLocalAuthor();
private final FeedMatcher matcher = new FeedMatcherImpl();
@Test
public void testFeedWithMatchingUrlIsChosen() {
RssProperties candidate = new RssProperties(URL,
TITLE, DESCRIPTION, AUTHOR, LINK, URI);
// The first feed has a different/null URL but matching RSS fields
Feed feed1 = createFeed(new RssProperties(nope(),
TITLE, DESCRIPTION, AUTHOR, LINK, URI));
// The second feed has a matching URL but different/null RSS fields
Feed feed2 = createFeed(new RssProperties(URL,
nope(), nope(), nope(), nope(), nope()));
Feed match = matcher.findMatchingFeed(candidate, asList(feed1, feed2));
// The matcher should choose the feed with the matching URL
assertNotNull(match);
assertSame(feed2, match);
}
@Test
public void testNullUrlIsNotMatched() {
// The candidate has a null URL
RssProperties candidate = new RssProperties(null,
TITLE, DESCRIPTION, AUTHOR, LINK, URI);
// The first feed has a non-null URL and matching RSS fields
Feed feed1 = createFeed(new RssProperties(URL,
TITLE, DESCRIPTION, AUTHOR, LINK, URI));
// The second feed has a null URL and different/null RSS fields
Feed feed2 = createFeed(new RssProperties(null,
nope(), nope(), nope(), nope(), nope()));
Feed match = matcher.findMatchingFeed(candidate, asList(feed1, feed2));
// The matcher should choose the feed with the matching RSS fields
assertNotNull(match);
assertSame(feed1, match);
}
@Test
public void testDoesNotMatchOneRssField() {
testDoesNotMatchRssFields(TITLE, nope(), nope(), nope(), nope());
testDoesNotMatchRssFields(nope(), DESCRIPTION, nope(), nope(), nope());
testDoesNotMatchRssFields(nope(), nope(), AUTHOR, nope(), nope());
testDoesNotMatchRssFields(nope(), nope(), nope(), LINK, nope());
testDoesNotMatchRssFields(nope(), nope(), nope(), nope(), URL);
}
private void testDoesNotMatchRssFields(String title, String description,
String author, String link, String uri) {
RssProperties candidate = new RssProperties(null,
TITLE, DESCRIPTION, AUTHOR, LINK, URL);
// The first feed has no matching RSS fields
Feed feed1 = createFeed(new RssProperties(null,
nope(), nope(), nope(), nope(), nope()));
// The second feed has the given RSS fields
Feed feed2 = createFeed(new RssProperties(null,
title, description, author, link, uri));
Feed match = matcher.findMatchingFeed(candidate, asList(feed1, feed2));
// The matcher should not choose either of the feeds
assertNull(match);
}
@Test
public void testMatchesTwoRssFields() {
testMatchesRssFields(TITLE, DESCRIPTION, nope(), nope(), nope());
testMatchesRssFields(nope(), DESCRIPTION, AUTHOR, nope(), nope());
testMatchesRssFields(nope(), nope(), AUTHOR, LINK, nope());
testMatchesRssFields(nope(), nope(), nope(), LINK, URI);
}
private void testMatchesRssFields(String title, String description,
String author, String link, String uri) {
RssProperties candidate = new RssProperties(null,
TITLE, DESCRIPTION, AUTHOR, LINK, URI);
// The first feed has no matching RSS fields
Feed feed1 = createFeed(new RssProperties(null,
nope(), nope(), nope(), nope(), nope()));
// The second feed has the given RSS fields
Feed feed2 = createFeed(new RssProperties(null,
title, description, author, link, uri));
// The third feed has one matching RSS field
Feed feed3 = createFeed(new RssProperties(null,
TITLE, nope(), nope(), nope(), nope()));
FeedMatcher matcher = new FeedMatcherImpl();
Feed match = matcher.findMatchingFeed(candidate,
asList(feed1, feed2, feed3));
// The matcher should choose the second feed
assertSame(feed2, match);
}
@Test
public void testFeedWithMostMatchingRssFieldsIsChosen() {
RssProperties candidate = new RssProperties(null,
TITLE, DESCRIPTION, AUTHOR, LINK, URI);
// The first feed has no matching RSS fields
Feed feed1 = createFeed(new RssProperties(null,
nope(), nope(), nope(), nope(), nope()));
// The second feed has three matching RSS fields
Feed feed2 = createFeed(new RssProperties(null,
TITLE, DESCRIPTION, AUTHOR, nope(), nope()));
// The third feed has two matching RSS fields
Feed feed3 = createFeed(new RssProperties(null,
TITLE, DESCRIPTION, nope(), nope(), nope()));
Feed match = matcher.findMatchingFeed(candidate,
asList(feed1, feed2, feed3));
// The matcher should choose the second feed
assertSame(feed2, match);
}
@Test
public void testNullRssFieldsAreNotMatched() {
// The candidate has a null URL and null RSS fields
RssProperties candidate = new RssProperties(null,
null, null, null, null, null);
// The first feed has a null URL and non-null RSS fields
Feed feed1 = createFeed(new RssProperties(null,
TITLE, DESCRIPTION, AUTHOR, LINK, URI));
// The second feed has a non-null URL and null RSS fields
Feed feed2 = createFeed(new RssProperties(URL,
null, null, null, null, null));
Feed match = matcher.findMatchingFeed(candidate, asList(feed1, feed2));
// The matcher should not choose either of the feeds
assertNull(match);
}
/**
* Returns an RSS field that doesn't match the default, either because it's
* null or because it's a different non-null value.
*/
private String nope() {
return random.nextBoolean() ? null : "x";
}
private Feed createFeed(RssProperties properties) {
Blog blog = new Blog(getGroup(clientId, 123), localAuthor, true);
return new Feed(blog, localAuthor, properties, 0, 0, 0);
}
}

View File

@@ -0,0 +1,95 @@
<feed xmlns="http://www.w3.org/2005/Atom">
<title>News - Briar</title>
<link href="https://briarproject.org/news/index.xml" rel="self"/>
<link href="https://briarproject.org/news/"/>
<updated>2022-12-05T12:00:00+00:00</updated>
<id>https://briarproject.org/news/</id>
<author>
<name>The Briar Team</name>
</author>
<generator>Hugo -- gohugo.io</generator>
<entry>
<title type="html"><![CDATA[Briar Desktop got another round of funding]]></title>
<link href="https://briarproject.org/news/2022-briar-desktop-nlnet-funding/"/>
<id>https://briarproject.org/news/2022-briar-desktop-nlnet-funding/</id>
<published>2022-12-05T12:00:00+00:00</published>
<updated>2022-12-05T12:00:00+00:00</updated>
<summary><![CDATA[Briar Desktop got another round of funding So far, Briar is only available as an Android app, which is preventing some organizations that work in repressive environments from using it as a secure communications tool and considering it as a more secure alternative to email. To remedy that, we have started working on a desktop app in September 2021 that is supposed to work on three major operating systems: Linux, macOS and Windows.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar is available on Google Play again]]></title>
<link href="https://briarproject.org/news/2022-briar-removed-from-google-play/"/>
<id>https://briarproject.org/news/2022-briar-removed-from-google-play/</id>
<published>2022-02-28T13:20:00+00:00</published>
<updated>2022-02-28T13:20:00+00:00</updated>
<summary><![CDATA[Status update Update (February 28, 13:20 UTC): Briar is available on Google Play again.
Briar was briefly removed from Google Play because we didn&rsquo;t provide Google&rsquo;s review team with a username and password for testing the app. We provided Google with a username and password for testing and the app is now available again.
About Briar Briar is a messaging app designed for activists, journalists, and anyone else who needs a safe, easy and robust way to communicate.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar 1.4 released - offline app sharing, message transfer via SD cards and USB sticks]]></title>
<link href="https://briarproject.org/news/2021-briar-1.4-released/"/>
<id>https://briarproject.org/news/2021-briar-1.4-released/</id>
<published>2021-11-15T00:00:00+00:00</published>
<updated>2021-11-15T00:00:00+00:00</updated>
<summary><![CDATA[Press release The Briar Project released version 1.4 of its Android app today. This release adds a couple of new features, highlighted below.
First of all, users can now share the app offline. Prior to this release, the only way to get the app was to to download it from the internet, which requires an internet connection. Now, it is possible to share the app offline to others who don&rsquo;t have it installed.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar 1.3 released - image attachments, profile images and disappearing messages]]></title>
<link href="https://briarproject.org/news/2021-briar-1.3-released/"/>
<id>https://briarproject.org/news/2021-briar-1.3-released/</id>
<published>2021-06-07T00:00:00+00:00</published>
<updated>2021-06-07T00:00:00+00:00</updated>
<summary><![CDATA[Press release The Briar Project released version 1.3 of its Android app today. Thanks to support from eQualit.ie, this release adds several new features that have been requested by many users over the years.
With today&rsquo;s release, users can upload profile pictures that will be visible only to their contacts.
Lots of people have asked for a way to send images via Briar. We listened! This release adds the ability to send images in private conversations.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar 1.2 released, contacts can now be added by exchanging links]]></title>
<link href="https://briarproject.org/news/2019-briar-1.2-released-remote-contacts/"/>
<id>https://briarproject.org/news/2019-briar-1.2-released-remote-contacts/</id>
<published>2019-11-06T00:00:00+00:00</published>
<updated>2019-11-06T00:00:00+00:00</updated>
<summary><![CDATA[Press Release The Briar Project released version 1.2 of its Android app today. This release allows users to add each other securely by exchanging links. Previously users needed to meet in person or ask a mutual contact to introduce them.
Most messenger apps find your contacts by uploading your phone&rsquo;s contact list to a server. Since Briar is designed to protect metadata and contact relationships, it instead uses the Tor network to connect directly and securely to the person you&rsquo;re adding, without revealing your contact list to anyone.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar 1.1 released with dark theme, new emoji and more]]></title>
<link href="https://briarproject.org/news/2018-briar-1.1-released/"/>
<id>https://briarproject.org/news/2018-briar-1.1-released/</id>
<published>2018-09-14T00:00:00+00:00</published>
<updated>2018-09-14T00:00:00+00:00</updated>
<summary><![CDATA[Press Release The Briar Project released version 1.1 of its Android app today. This release adds new features following the app&rsquo;s first public release in May.
Thanks to support from the Open Technology Fund, the new release has a dark theme designed by Ura Design. Users can switch between the light and dark themes, or use an automatic mode that activates the dark theme at night. The conversation screen has also been redesigned, with rounded message bubbles and a new color scheme.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar - Secure P2P Messenger Releases First Version, Receives New Funding]]></title>
<link href="https://briarproject.org/news/2018-1.0-released-new-funding/"/>
<id>https://briarproject.org/news/2018-1.0-released-new-funding/</id>
<published>2018-05-09T00:00:00+00:00</published>
<updated>2018-05-09T00:00:00+00:00</updated>
<summary><![CDATA[Press Release The peer-to-peer messenger Briar released its first stable version today. It is available for Android devices from Google Play or F-Droid. This release follows a security audit and a 10 month public beta period during which many bugs were fixed and lots of feedback was received. The Briar Project wishes to thank all beta testers for their contributions.
The development of Briar will continue with help from the Open Technology Fund, which has previously supported the project as part of its mission to promote internet freedom worldwide.]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Briar - Darknet Messenger Releases Beta, Passes Security Audit]]></title>
<link href="https://briarproject.org/news/2017-beta-released-security-audit/"/>
<id>https://briarproject.org/news/2017-beta-released-security-audit/</id>
<published>2017-07-21T00:00:00+00:00</published>
<updated>2017-07-21T00:00:00+00:00</updated>
<summary><![CDATA[Press Release After extensive private beta tests, the first public beta of Briar was released today. Briar is a secure messaging app for Android.
Unlike other popular apps, Briar does not require servers to work. It connects users directly using a peer-to-peer network. This makes it resistant to censorship and allows it to work even without internet access.
The app encrypts all data end-to-end and also hides metadata about who is communicating.]]></summary>
</entry>
</feed>