diff --git a/app/src/androidTest/java/im/conversations/android/xmpp/ArchivePagingTest.java b/app/src/androidTest/java/im/conversations/android/xmpp/ArchivePagingTest.java new file mode 100644 index 000000000..0c4d2a80c --- /dev/null +++ b/app/src/androidTest/java/im/conversations/android/xmpp/ArchivePagingTest.java @@ -0,0 +1,129 @@ +package im.conversations.android.xmpp; + +import static org.hamcrest.Matchers.*; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.Iterables; +import im.conversations.android.database.model.Account; +import im.conversations.android.database.model.StanzaId; +import im.conversations.android.transformer.MessageTransformation; +import im.conversations.android.xmpp.manager.ArchiveManager; +import im.conversations.android.xmpp.model.jabber.Body; +import im.conversations.android.xmpp.model.stanza.Message; +import java.time.Instant; +import java.util.concurrent.ExecutionException; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ArchivePagingTest extends BaseTransformationTest { + + @Test + public void initialQuery() throws ExecutionException, InterruptedException { + final var ranges = database.archiveDao().resetLivePage(account(), ACCOUNT); + final Range range = Iterables.getOnlyElement(ranges); + Assert.assertNull(range.id); + Assert.assertEquals(Range.Order.REVERSE, range.order); + } + + @Test + public void queryAfterSingleLiveMessage() throws ExecutionException, InterruptedException { + final var stub = new StubMessage(2); + transformer.transform(stub.messageTransformation(), stub.stanzaId()); + final var ranges = database.archiveDao().resetLivePage(account(), ACCOUNT); + Assert.assertEquals(2, ranges.size()); + MatcherAssert.assertThat( + ranges, + contains(new Range(Range.Order.REVERSE, "2"), new Range(Range.Order.NORMAL, "2"))); + } + + @Test + public void twoLiveMessageQueryNoSubmitAndQuery() + throws ExecutionException, InterruptedException { + final var stub2 = new StubMessage(2); + transformer.transform(stub2.messageTransformation(), stub2.stanzaId()); + final var stub3 = new StubMessage(3); + transformer.transform(stub3.messageTransformation(), stub3.stanzaId()); + + final var ranges = database.archiveDao().resetLivePage(account(), ACCOUNT); + Assert.assertEquals(2, ranges.size()); + MatcherAssert.assertThat( + ranges, + contains(new Range(Range.Order.REVERSE, "2"), new Range(Range.Order.NORMAL, "3"))); + + final var stub4 = new StubMessage(4); + transformer.transform(stub4.messageTransformation(), stub4.stanzaId()); + + final var rangesSecondAttempt = database.archiveDao().resetLivePage(account(), ACCOUNT); + Assert.assertEquals(2, rangesSecondAttempt.size()); + MatcherAssert.assertThat( + rangesSecondAttempt, + contains(new Range(Range.Order.REVERSE, "2"), new Range(Range.Order.NORMAL, "3"))); + } + + @Test + public void liveMessageQuerySubmitAndQuery() throws ExecutionException, InterruptedException { + final var stub2 = new StubMessage(2); + transformer.transform(stub2.messageTransformation(), stub2.stanzaId()); + final var stub3 = new StubMessage(3); + transformer.transform(stub3.messageTransformation(), stub3.stanzaId()); + + final var ranges = database.archiveDao().resetLivePage(account(), ACCOUNT); + Assert.assertEquals(2, ranges.size()); + MatcherAssert.assertThat( + ranges, + contains(new Range(Range.Order.REVERSE, "2"), new Range(Range.Order.NORMAL, "3"))); + + final var stub4 = new StubMessage(4); + transformer.transform(stub4.messageTransformation(), stub4.stanzaId()); + + for (final Range range : ranges) { + database.archiveDao() + .submitPage( + account(), + ACCOUNT, + range, + new ArchiveManager.QueryResult( + true, Page.emptyWithCount(range.id, null)), + false); + } + + final var rangesSecondAttempt = database.archiveDao().resetLivePage(account(), ACCOUNT); + // we mark the reversing range as complete in the submit above; hence it is not included in + // the second ranges + Assert.assertEquals(1, rangesSecondAttempt.size()); + MatcherAssert.assertThat(rangesSecondAttempt, contains(new Range(Range.Order.NORMAL, "4"))); + } + + private Account account() throws ExecutionException, InterruptedException { + return this.database.accountDao().getEnabledAccount(ACCOUNT).get(); + } + + private static class StubMessage { + public final int id; + + private StubMessage(int id) { + this.id = id; + } + + public StanzaId stanzaId() { + return new StanzaId(String.valueOf(id), ACCOUNT); + } + + public MessageTransformation messageTransformation() { + final var message = new Message(); + message.setTo(ACCOUNT); + message.setFrom(REMOTE); + message.addExtension(new Body()).setContent(String.format("%s (%d)", GREETING, id)); + return MessageTransformation.of( + message, + Instant.ofEpochSecond(id * 2000L), + REMOTE, + String.valueOf(id), + message.getFrom().asBareJid(), + null); + } + } +} diff --git a/app/src/androidTest/java/im/conversations/android/xmpp/BaseTransformationTest.java b/app/src/androidTest/java/im/conversations/android/xmpp/BaseTransformationTest.java new file mode 100644 index 000000000..cf43eb666 --- /dev/null +++ b/app/src/androidTest/java/im/conversations/android/xmpp/BaseTransformationTest.java @@ -0,0 +1,42 @@ +package im.conversations.android.xmpp; + +import android.content.Context; +import androidx.room.Room; +import androidx.test.core.app.ApplicationProvider; +import im.conversations.android.IDs; +import im.conversations.android.database.ConversationsDatabase; +import im.conversations.android.database.entity.AccountEntity; +import im.conversations.android.transformer.Transformer; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.impl.JidCreate; + +public abstract class BaseTransformationTest { + + protected static final BareJid ACCOUNT = JidCreate.bareFromOrThrowUnchecked("user@example.com"); + protected static final BareJid REMOTE = + JidCreate.bareFromOrThrowUnchecked("juliet@example.com"); + protected static final BareJid REMOTE_2 = + JidCreate.bareFromOrThrowUnchecked("romeo@example.com"); + + protected static final String GREETING = "Hi Juliet. How are you?"; + + protected ConversationsDatabase database; + protected Transformer transformer; + + @Before + public void setupTransformer() throws ExecutionException, InterruptedException { + final Context context = ApplicationProvider.getApplicationContext(); + this.database = Room.inMemoryDatabaseBuilder(context, ConversationsDatabase.class).build(); + final var account = new AccountEntity(); + account.address = ACCOUNT; + account.enabled = true; + account.randomSeed = IDs.seed(); + final long id = database.accountDao().insert(account); + + this.transformer = + new Transformer( + database.accountDao().getEnabledAccount(id).get(), context, database); + } +} diff --git a/app/src/androidTest/java/im/conversations/android/xmpp/MessageTransformationTest.java b/app/src/androidTest/java/im/conversations/android/xmpp/MessageTransformationTest.java index 2597d4ec7..ee17d63b5 100644 --- a/app/src/androidTest/java/im/conversations/android/xmpp/MessageTransformationTest.java +++ b/app/src/androidTest/java/im/conversations/android/xmpp/MessageTransformationTest.java @@ -1,19 +1,12 @@ package im.conversations.android.xmpp; -import android.content.Context; -import androidx.room.Room; -import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.Iterables; -import im.conversations.android.IDs; -import im.conversations.android.database.ConversationsDatabase; -import im.conversations.android.database.entity.AccountEntity; import im.conversations.android.database.model.Encryption; import im.conversations.android.database.model.MessageEmbedded; import im.conversations.android.database.model.Modification; import im.conversations.android.database.model.PartType; import im.conversations.android.transformer.MessageTransformation; -import im.conversations.android.transformer.Transformer; import im.conversations.android.xmpp.model.correction.Replace; import im.conversations.android.xmpp.model.jabber.Body; import im.conversations.android.xmpp.model.reactions.Reaction; @@ -23,42 +16,15 @@ import im.conversations.android.xmpp.model.reply.Reply; import im.conversations.android.xmpp.model.retract.Retract; import im.conversations.android.xmpp.model.stanza.Message; import java.time.Instant; -import java.util.concurrent.ExecutionException; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.jxmpp.jid.BareJid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; import org.jxmpp.stringprep.XmppStringprepException; @RunWith(AndroidJUnit4.class) -public class MessageTransformationTest { - - private static final BareJid ACCOUNT = JidCreate.bareFromOrThrowUnchecked("user@example.com"); - private static final BareJid REMOTE = JidCreate.bareFromOrThrowUnchecked("juliet@example.com"); - private static final BareJid REMOTE_2 = JidCreate.bareFromOrThrowUnchecked("romeo@example.com"); - - private static final String GREETING = "Hi Juliet. How are you?"; - - private ConversationsDatabase database; - private Transformer transformer; - - @Before - public void setupTransformer() throws ExecutionException, InterruptedException { - final Context context = ApplicationProvider.getApplicationContext(); - this.database = Room.inMemoryDatabaseBuilder(context, ConversationsDatabase.class).build(); - final var account = new AccountEntity(); - account.address = ACCOUNT; - account.enabled = true; - account.randomSeed = IDs.seed(); - final long id = database.accountDao().insert(account); - - this.transformer = - new Transformer( - database.accountDao().getEnabledAccount(id).get(), context, database); - } +public class MessageTransformationTest extends BaseTransformationTest { @Test public void reactionBeforeOriginal() throws XmppStringprepException { diff --git a/app/src/main/java/im/conversations/android/database/dao/ArchiveDao.java b/app/src/main/java/im/conversations/android/database/dao/ArchiveDao.java index e2152c15b..240ee5951 100644 --- a/app/src/main/java/im/conversations/android/database/dao/ArchiveDao.java +++ b/app/src/main/java/im/conversations/android/database/dao/ArchiveDao.java @@ -137,6 +137,7 @@ public abstract class ArchiveDao extends BaseDao { final var existingLivePage = getPage(account.id, archive, ArchivePageEntity.Type.LIVE); if (existingLivePage != null) { existingLivePage.start = page.last; + insert(existingLivePage); } else { insert( ArchivePageEntity.of( diff --git a/app/src/main/java/im/conversations/android/xmpp/Range.java b/app/src/main/java/im/conversations/android/xmpp/Range.java index 8f8b3e51d..8aff5094e 100644 --- a/app/src/main/java/im/conversations/android/xmpp/Range.java +++ b/app/src/main/java/im/conversations/android/xmpp/Range.java @@ -2,6 +2,7 @@ package im.conversations.android.xmpp; import androidx.annotation.NonNull; import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; public class Range { @@ -19,6 +20,19 @@ public class Range { return MoreObjects.toStringHelper(this).add("order", order).add("id", id).toString(); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Range range = (Range) o; + return order == range.order && Objects.equal(id, range.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(order, id); + } + public enum Order { NORMAL, REVERSE diff --git a/app/src/main/java/im/conversations/android/xmpp/manager/ArchiveManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/ArchiveManager.java index b1e39e5b3..3398fb75f 100644 --- a/app/src/main/java/im/conversations/android/xmpp/manager/ArchiveManager.java +++ b/app/src/main/java/im/conversations/android/xmpp/manager/ArchiveManager.java @@ -251,7 +251,7 @@ public class ArchiveManager extends AbstractManager { } } - public final class QueryResult { + public static final class QueryResult { public final boolean isComplete; public final Page page;