diff --git a/app/build.gradle b/app/build.gradle index a84ce6b..fa293b4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,6 +85,11 @@ dependencies { testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.8.9' + testCompile 'org.hamcrest:hamcrest-library:1.3' + testCompile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + testCompile 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0' + testCompile 'org.robolectric:robolectric:3.4.2' + testCompile "org.robolectric:shadows-multidex:3.4-rc2" } idea.module { diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.kt b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.kt index e65026b..15f7c7a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.kt +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.kt @@ -113,7 +113,7 @@ class AndroidInventory(private val sql: SqlHelper) : Inventory { where.append(" AND version = ").append(version) } if (types.isNotEmpty()) { - where.append(" AND type IN (").append(join(*types)).append(")") + where.append(" AND type IN (").append(types.joinToString(separator = "', '", prefix = "'", postfix = "'", transform = { it.number.toString() })).append(")") } val db = sql.readableDatabase diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.kt b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.kt index 963393b..965285a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.kt +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.kt @@ -148,8 +148,9 @@ class AndroidMessageRepository(private val sql: SqlHelper, private val context: } val result = LinkedList() sql.readableDatabase.query( - TABLE_NAME, projection, - where, null, null, null, null + true, + TABLE_NAME, projection, where, + null, null, null, null, null ).use { c -> while (c.moveToNext()) { val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)) @@ -172,8 +173,7 @@ class AndroidMessageRepository(private val sql: SqlHelper, private val context: // save new parents var order = 0 for (parentIV in message.parents) { - val parent = getMessage(parentIV) - if (parent != null) { + getMessage(parentIV)?.let { parent -> mergeConversations(db, parent.conversationId, message.conversationId) order++ val values = ContentValues() @@ -198,9 +198,9 @@ class AndroidMessageRepository(private val sql: SqlHelper, private val context: private fun mergeConversations(db: SQLiteDatabase, source: UUID, target: UUID) { val values = ContentValues() values.put("conversation", UuidUtils.asBytes(target)) - val whereArgs = arrayOf(hex(UuidUtils.asBytes(source))) - db.update(TABLE_NAME, values, "conversation=?", whereArgs) - db.update(PARENTS_TABLE_NAME, values, "conversation=?", whereArgs) + val where = "conversation=X'${hex(UuidUtils.asBytes(source))}'" + db.update(TABLE_NAME, values, where, null) + db.update(PARENTS_TABLE_NAME, values, where, null) } private fun findIds(where: String): List { diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.kt b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.kt index 6a21a02..f16cde0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.kt +++ b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.kt @@ -99,6 +99,6 @@ class SqlHelper(private val ctx: Context) : SQLiteOpenHelper(ctx, DATABASE_NAME, private val DATABASE_VERSION = 7 val DATABASE_NAME = "jabit.db" - internal fun join(vararg types: Enum<*>): String = types.joinToString(separator = "', '", prefix = "'", postfix = "'", transform = {it.name}) + internal fun join(vararg types: Enum<*>): String = types.joinToString(separator = "', '", prefix = "'", postfix = "'", transform = { it.name }) } } diff --git a/app/src/test/java/ch/dissem/bitmessage/repository/AndroidAddressRepositoryTest.kt b/app/src/test/java/ch/dissem/bitmessage/repository/AndroidAddressRepositoryTest.kt new file mode 100644 index 0000000..dfcba68 --- /dev/null +++ b/app/src/test/java/ch/dissem/bitmessage/repository/AndroidAddressRepositoryTest.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.repository + +import android.os.Build +import ch.dissem.apps.abit.BuildConfig +import ch.dissem.apps.abit.repository.AndroidAddressRepository +import ch.dissem.apps.abit.repository.SqlHelper +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = intArrayOf(Build.VERSION_CODES.LOLLIPOP), packageName = "ch.dissem.apps.abit") +class AndroidAddressRepositoryTest : TestBase() { + private val CONTACT_A = "BM-2cW7cD5cDQJDNkE7ibmyTxfvGAmnPqa9Vt" + private val CONTACT_B = "BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj" + private val CONTACT_C = "BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke" + + private lateinit var IDENTITY_A: String + private lateinit var IDENTITY_B: String + + private lateinit var repo: AndroidAddressRepository + + @Before + fun setUp() { + RuntimeEnvironment.application.deleteDatabase(SqlHelper.DATABASE_NAME) + val sqlHelper = SqlHelper(RuntimeEnvironment.application) + + repo = AndroidAddressRepository(sqlHelper) + + repo.save(BitmessageAddress(CONTACT_A)) + repo.save(BitmessageAddress(CONTACT_B)) + repo.save(BitmessageAddress(CONTACT_C)) + + val identityA = BitmessageAddress(PrivateKey(false, 1, 1000, 1000, DOES_ACK)) + repo.save(identityA) + IDENTITY_A = identityA.address + val identityB = BitmessageAddress(PrivateKey(false, 1, 1000, 1000)) + repo.save(identityB) + IDENTITY_B = identityB.address + } + + @Test + fun `ensure contact can be found`() { + val address = BitmessageAddress(CONTACT_A) + assertEquals(4, address.version) + assertEquals(address, repo.findContact(address.tag!!)) + assertNull(repo.findIdentity(address.tag!!)) + } + + @Test + fun `ensure identity can be found`() { + val identity = BitmessageAddress(IDENTITY_A) + assertEquals(4, identity.version) + assertNull(repo.findContact(identity.tag!!)) + + val storedIdentity = repo.findIdentity(identity.tag!!) + assertEquals(identity, storedIdentity) + assertTrue(storedIdentity!!.has(DOES_ACK)) + } + + @Test + fun `ensure identities are retrieved`() { + val identities = repo.getIdentities() + assertEquals(2, identities.size.toLong()) + for (identity in identities) { + assertNotNull(identity.privateKey) + } + } + + @Test + fun `ensure subscriptions are retrieved`() { + addSubscription("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h") + addSubscription("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ") + addSubscription("BM-2D9QKN4teYRvoq2fyzpiftPh9WP9qggtzh") + val subscriptions = repo.getSubscriptions() + assertEquals(3, subscriptions.size.toLong()) + } + + @Test + fun `ensure subscriptions are retrieved for given version`() { + addSubscription("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h") + addSubscription("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ") + addSubscription("BM-2D9QKN4teYRvoq2fyzpiftPh9WP9qggtzh") + + var subscriptions = repo.getSubscriptions(5) + + assertEquals(1, subscriptions.size.toLong()) + + subscriptions = repo.getSubscriptions(4) + assertEquals(2, subscriptions.size.toLong()) + } + + @Test + fun `ensure contacts are retrieved`() { + val contacts = repo.getContacts() + assertEquals(3, contacts.size.toLong()) + for (contact in contacts) { + assertNull(contact.privateKey) + } + } + + @Test + fun `ensure new address is saved`() { + repo.save(BitmessageAddress(PrivateKey(false, 1, 1000, 1000))) + val identities = repo.getIdentities() + assertEquals(3, identities.size.toLong()) + } + + @Test + fun `ensure existing address is updated`() { + var address = repo.getAddress(CONTACT_A) + address!!.alias = "Test-Alias" + repo.save(address) + address = repo.getAddress(address.address) + assertEquals("Test-Alias", address!!.alias) + } + + @Test + fun `ensure existing keys are not deleted`() { + val address = BitmessageAddress(IDENTITY_A) + address.alias = "Test" + repo.save(address) + val identityA = repo.getAddress(IDENTITY_A) + assertNotNull(identityA!!.pubkey) + assertNotNull(identityA.privateKey) + assertEquals("Test", identityA.alias) + assertFalse(identityA.isChan) + } + + @Test + fun `ensure new chan is saved and updated`() { + val chan = BitmessageAddress.chan(1, "test") + repo.save(chan) + var address = repo.getAddress(chan.address) + assertNotNull(address) + assertTrue(address!!.isChan) + + address.alias = "Test" + repo.save(address) + + address = repo.getAddress(chan.address) + assertNotNull(address) + assertTrue(address!!.isChan) + assertEquals("Test", address.alias) + } + + @Test + fun `ensure address is removed`() { + val address = repo.getAddress(IDENTITY_A) + repo.remove(address!!) + assertNull(repo.getAddress(IDENTITY_A)) + } + + @Test + fun `ensure address can be retrieved`() { + val address = repo.getAddress(IDENTITY_A) + assertNotNull(address) + assertNotNull(address!!.privateKey) + } + + private fun addSubscription(address: String) { + val subscription = BitmessageAddress(address) + subscription.isSubscribed = true + repo.save(subscription) + } +} diff --git a/app/src/test/java/ch/dissem/bitmessage/repository/AndroidInventoryTest.kt b/app/src/test/java/ch/dissem/bitmessage/repository/AndroidInventoryTest.kt new file mode 100644 index 0000000..de5ac7b --- /dev/null +++ b/app/src/test/java/ch/dissem/bitmessage/repository/AndroidInventoryTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.repository + +import android.os.Build +import ch.dissem.apps.abit.BuildConfig +import ch.dissem.apps.abit.repository.AndroidInventory +import ch.dissem.apps.abit.repository.SqlHelper +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.payload.GetPubkey +import ch.dissem.bitmessage.entity.payload.ObjectPayload +import ch.dissem.bitmessage.entity.payload.ObjectType.GET_PUBKEY +import ch.dissem.bitmessage.entity.payload.ObjectType.MSG +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.ports.Inventory +import ch.dissem.bitmessage.utils.UnixTime.DAY +import ch.dissem.bitmessage.utils.UnixTime.now +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import java.util.* + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = intArrayOf(Build.VERSION_CODES.LOLLIPOP), packageName = "ch.dissem.apps.abit") +class AndroidInventoryTest : TestBase() { + private lateinit var inventory: Inventory + + private lateinit var inventoryVector1: InventoryVector + private lateinit var inventoryVector2: InventoryVector + private lateinit var inventoryVectorIgnore: InventoryVector + + @Before + fun setUp() { + RuntimeEnvironment.application.deleteDatabase(SqlHelper.DATABASE_NAME) + val sqlHelper = SqlHelper(RuntimeEnvironment.application) + + inventory = AndroidInventory(sqlHelper) + + val object1 = getObjectMessage(1, 300, getPubkey) + inventoryVector1 = object1.inventoryVector + inventory.storeObject(object1) + + val object2 = getObjectMessage(2, 300, getPubkey) + inventoryVector2 = object2.inventoryVector + inventory.storeObject(object2) + + val ignore = getObjectMessage(1, -1 * DAY, getPubkey) + inventoryVectorIgnore = ignore.inventoryVector + inventory.storeObject(ignore) + } + + @Test + fun `ensure inventory can be retrieved`() { + var inventoryVectors = inventory.getInventory(1) + assertEquals(1, inventoryVectors.size.toLong()) + + inventoryVectors = inventory.getInventory(2) + assertEquals(1, inventoryVectors.size.toLong()) + } + + @Test + fun `ensure the IVs of missing objects are returned`() { + val newIV = getObjectMessage(1, 200, getPubkey).inventoryVector + val offer = LinkedList() + offer.add(newIV) + offer.add(inventoryVector1) + val missing = inventory.getMissing(offer, 1, 2) + assertEquals(1, missing.size.toLong()) + assertEquals(newIV, missing[0]) + } + + @Test + fun `ensure single object can be retrieved`() { + val `object` = inventory.getObject(inventoryVectorIgnore) + assertNotNull(`object`) + assertEquals(1, `object`!!.stream) + assertEquals(inventoryVectorIgnore, `object`.inventoryVector) + } + + @Test + fun `ensure objects can be retrieved`() { + var objects = inventory.getObjects(1, 4) + assertEquals(2, objects.size.toLong()) + + objects = inventory.getObjects(1, 4, GET_PUBKEY) + assertEquals(2, objects.size.toLong()) + + objects = inventory.getObjects(1, 4, MSG) + assertEquals(0, objects.size.toLong()) + } + + @Test + fun `ensure object can be stored`() { + val `object` = getObjectMessage(5, 0, getPubkey) + inventory.storeObject(`object`) + + assertNotNull(inventory.getObject(`object`.inventoryVector)) + } + + @Test + fun `ensure contained objects are recognized`() { + val `object` = getObjectMessage(5, 0, getPubkey) + + assertFalse(inventory.contains(`object`)) + + inventory.storeObject(`object`) + + assertTrue(inventory.contains(`object`)) + } + + @Test + fun `ensure inventory is cleaned up`() { + assertNotNull(inventory.getObject(inventoryVectorIgnore)) + inventory.cleanup() + assertNull(inventory.getObject(inventoryVectorIgnore)) + } + + private fun getObjectMessage(stream: Long, TTL: Long, payload: ObjectPayload): ObjectMessage { + return ObjectMessage( + nonce = ByteArray(8), + expiresTime = now + TTL, + stream = stream, + payload = payload + ) + } + + private val getPubkey: GetPubkey = GetPubkey(BitmessageAddress("BM-2cW7cD5cDQJDNkE7ibmyTxfvGAmnPqa9Vt")) +} diff --git a/app/src/test/java/ch/dissem/bitmessage/repository/AndroidMessageRepositoryTest.kt b/app/src/test/java/ch/dissem/bitmessage/repository/AndroidMessageRepositoryTest.kt new file mode 100644 index 0000000..b1ab112 --- /dev/null +++ b/app/src/test/java/ch/dissem/bitmessage/repository/AndroidMessageRepositoryTest.kt @@ -0,0 +1,346 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.repository + +import android.os.Build +import ch.dissem.apps.abit.BuildConfig +import ch.dissem.apps.abit.repository.AndroidAddressRepository +import ch.dissem.apps.abit.repository.AndroidMessageRepository +import ch.dissem.apps.abit.repository.SqlHelper +import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.Plaintext.Type.MSG +import ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding +import ch.dissem.bitmessage.entity.valueobject.Label +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.entity.valueobject.extended.Message +import ch.dissem.bitmessage.ports.MessageRepository +import ch.dissem.bitmessage.utils.UnixTime +import org.hamcrest.BaseMatcher +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.Matchers.* +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import java.util.* + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = intArrayOf(Build.VERSION_CODES.LOLLIPOP), packageName = "ch.dissem.apps.abit") +class AndroidMessageRepositoryTest : TestBase() { + private lateinit var contactA: BitmessageAddress + private lateinit var contactB: BitmessageAddress + private lateinit var identity: BitmessageAddress + + private lateinit var repo: MessageRepository + + private lateinit var inbox: Label + private lateinit var sent: Label + private lateinit var drafts: Label + private lateinit var unread: Label + + @Before + fun setUp() { + RuntimeEnvironment.application.deleteDatabase(SqlHelper.DATABASE_NAME) + val sqlHelper = SqlHelper(RuntimeEnvironment.application) + + val addressRepo = AndroidAddressRepository(sqlHelper) + repo = AndroidMessageRepository(sqlHelper, RuntimeEnvironment.application) + mockedInternalContext( + cryptography = SpongyCryptography(), + addressRepository = addressRepo, + messageRepository = repo, + port = 12345, + connectionTTL = 10, + connectionLimit = 10 + ) + val tmp = BitmessageAddress(PrivateKey(false, 1, 1000, 1000, DOES_ACK)) + contactA = BitmessageAddress(tmp.address) + contactA.pubkey = tmp.pubkey + addressRepo.save(contactA) + contactB = BitmessageAddress("BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj") + addressRepo.save(contactB) + + identity = BitmessageAddress(PrivateKey(false, 1, 1000, 1000, DOES_ACK)) + addressRepo.save(identity) + + inbox = repo.getLabels(Label.Type.INBOX)[0] + sent = repo.getLabels(Label.Type.SENT)[0] + drafts = repo.getLabels(Label.Type.DRAFT)[0] + unread = repo.getLabels(Label.Type.UNREAD)[0] + + addMessage(contactA, identity, Plaintext.Status.RECEIVED, inbox, unread) + addMessage(identity, contactA, Plaintext.Status.DRAFT, drafts) + addMessage(identity, contactB, Plaintext.Status.DRAFT, unread) + } + + @Test + fun `ensure labels are retrieved`() { + val labels = repo.getLabels() + assertEquals(7, labels.size.toLong()) + } + + @Test + fun `ensure labels can be retrieved by type`() { + val labels = repo.getLabels(Label.Type.INBOX) + assertEquals(1, labels.size.toLong()) + assertEquals("Inbox", labels[0].toString()) + } + + @Test + fun `ensure messages can be found by label`() { + val messages = repo.findMessages(inbox) + assertEquals(1, messages.size.toLong()) + val m = messages[0] + assertEquals(contactA, m.from) + assertEquals(identity, m.to) + assertEquals(Plaintext.Status.RECEIVED, m.status) + } + + @Test + fun `ensure unread messages can be found for all labels`() { + val unread = repo.countUnread(null) + assertThat(unread, `is`(2)) + } + + @Test + fun `ensure unread messages can be found by label`() { + val unread = repo.countUnread(inbox) + assertThat(unread, `is`(1)) + } + + @Test + fun `ensure message can be retrieved by initial hash`() { + val initialHash = ByteArray(64) + val message = repo.findMessages(contactA)[0] + message.initialHash = initialHash + repo.save(message) + val other = repo.getMessage(initialHash) + assertThat(other, `is`(message)) + } + + @Test + fun `ensure ack message can be updated and retrieved`() { + val initialHash = ByteArray(64) + val message = repo.findMessages(contactA)[0] + message.initialHash = initialHash + val ackMessage = message.ackMessage + repo.save(message) + val other = repo.getMessage(initialHash)!! + assertThat<Plaintext>(other, `is`(message)) + assertThat<ObjectMessage>(other.ackMessage, `is`<ObjectMessage>(ackMessage)) + } + + @Test + fun `ensure messages can be found by status`() { + val messages = repo.findMessages(Plaintext.Status.RECEIVED) + assertEquals(1, messages.size.toLong()) + val m = messages[0] + assertEquals(contactA, m.from) + assertEquals(identity, m.to) + assertEquals(Plaintext.Status.RECEIVED, m.status) + } + + @Test + fun `ensure messages can be found by status and recipient`() { + val messages = repo.findMessages(Plaintext.Status.DRAFT, contactB) + assertEquals(1, messages.size.toLong()) + val m = messages[0] + assertEquals(identity, m.from) + assertEquals(contactB, m.to) + assertEquals(Plaintext.Status.DRAFT, m.status) + } + + @Test + fun `ensure message can be saved`() { + val message = Plaintext.Builder(MSG) + .IV(randomInventoryVector()) + .from(identity) + .to(contactA) + .message("Subject", "Message") + .status(Plaintext.Status.DOING_PROOF_OF_WORK) + .build() + repo.save(message) + + assertNotNull(message.id) + + message.addLabels(inbox) + repo.save(message) + + val messages = repo.findMessages(Plaintext.Status.DOING_PROOF_OF_WORK) + + assertEquals(1, messages.size.toLong()) + assertNotNull(messages[0].inventoryVector) + } + + @Test + fun `ensure message can be updated`() { + var messages = repo.findMessages(Plaintext.Status.DRAFT, contactA) + val message = messages[0] + message.inventoryVector = randomInventoryVector() + repo.save(message) + + messages = repo.findMessages(Plaintext.Status.DRAFT, contactA) + assertEquals(1, messages.size.toLong()) + assertNotNull(messages[0].inventoryVector) + } + + @Test + fun `ensure message is removed`() { + val toRemove = repo.findMessages(Plaintext.Status.DRAFT, contactB)[0] + var messages = repo.findMessages(Plaintext.Status.DRAFT) + assertEquals(2, messages.size.toLong()) + repo.remove(toRemove) + messages = repo.findMessages(Plaintext.Status.DRAFT) + assertThat(messages, hasSize<Plaintext>(1)) + } + + @Test + fun `ensure unacknowledged messages are found for resend`() { + val message = Plaintext.Builder(MSG) + .IV(randomInventoryVector()) + .from(identity) + .to(contactA) + .message("Subject", "Message") + .sent(UnixTime.now) + .status(Plaintext.Status.SENT) + .ttl(2) + .build() + message.updateNextTry() + assertThat(message.retries, `is`(1)) + assertThat<Long>(message.nextTry, greaterThan(UnixTime.now)) + assertThat<Long>(message.nextTry, lessThanOrEqualTo(UnixTime.now + 2)) + repo.save(message) + Thread.sleep(4100) // somewhat longer than 2*TTL + var messagesToResend = repo.findMessagesToResend() + assertThat(messagesToResend, hasSize<Plaintext>(1)) + + message.updateNextTry() + assertThat(message.retries, `is`(2)) + assertThat<Long>(message.nextTry, greaterThan(UnixTime.now)) + repo.save(message) + messagesToResend = repo.findMessagesToResend() + assertThat(messagesToResend, empty<Plaintext>()) + } + + @Test + fun `ensure parents are saved`() { + val parent = storeConversation() + + val responses = repo.findResponses(parent) + assertThat(responses, hasSize<Plaintext>(2)) + assertThat(responses, hasItem(hasMessage("Re: new test", "Nice!"))) + assertThat(responses, hasItem(hasMessage("Re: new test", "PS: it did work!"))) + } + + @Test + fun `ensure conversation can be retrieved`() { + val root = storeConversation() + val conversations = repo.findConversations(inbox) + assertThat(conversations, hasSize<UUID>(2)) + assertThat(conversations, hasItem(root.conversationId)) + } + + private fun addMessage(from: BitmessageAddress, to: BitmessageAddress, status: Plaintext.Status, vararg labels: Label): Plaintext { + val content = Message.Builder() + .subject("Subject") + .body("Message") + .build() + return addMessage(from, to, content, status, *labels) + } + + private fun addMessage(from: BitmessageAddress, to: BitmessageAddress, + content: ExtendedEncoding, status: Plaintext.Status, vararg labels: Label): Plaintext { + val message = Plaintext.Builder(MSG) + .IV(randomInventoryVector()) + .from(from) + .to(to) + .message(content) + .status(status) + .labels(Arrays.asList(*labels)) + .build() + repo.save(message) + return message + } + + private fun storeConversation(): Plaintext { + val older = addMessage(identity, contactA, + Message.Builder() + .subject("hey there") + .body("does it work?") + .build(), + Plaintext.Status.SENT, sent) + + val root = addMessage(identity, contactA, + Message.Builder() + .subject("new test") + .body("There's a new test in town!") + .build(), + Plaintext.Status.SENT, sent) + + addMessage(contactA, identity, + Message.Builder() + .subject("Re: new test") + .body("Nice!") + .addParent(root) + .build(), + Plaintext.Status.RECEIVED, inbox) + + addMessage(contactA, identity, + Message.Builder() + .subject("Re: new test") + .body("PS: it did work!") + .addParent(root) + .addParent(older) + .build(), + Plaintext.Status.RECEIVED, inbox) + + return repo.getMessage(root.id!!) + } + + private fun hasMessage(subject: String?, body: String?): Matcher<Plaintext> { + return object : BaseMatcher<Plaintext>() { + override fun describeTo(description: Description) { + description.appendText("Subject: ").appendText(subject) + description.appendText(", ") + description.appendText("Body: ").appendText(body) + } + + override fun matches(item: Any): Boolean { + if (item is Plaintext) { + if (subject != null && subject != item.subject) { + return false + } + if (body != null && body != item.text) { + return false + } + return true + } else { + return false + } + } + } + } +} diff --git a/app/src/test/java/ch/dissem/bitmessage/repository/AndroidNodeRegistryTest.kt b/app/src/test/java/ch/dissem/bitmessage/repository/AndroidNodeRegistryTest.kt new file mode 100644 index 0000000..7027960 --- /dev/null +++ b/app/src/test/java/ch/dissem/bitmessage/repository/AndroidNodeRegistryTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.repository + +import android.os.Build +import ch.dissem.apps.abit.BuildConfig +import ch.dissem.apps.abit.repository.AndroidNodeRegistry +import ch.dissem.apps.abit.repository.SqlHelper +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress +import ch.dissem.bitmessage.ports.NodeRegistry +import ch.dissem.bitmessage.utils.UnixTime.now +import org.hamcrest.Matchers.empty +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import java.util.* + +/** + * Please note that some tests fail if there is no internet connection, + * as the initial nodes' IP addresses are determined by DNS lookup. + */ +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = intArrayOf(Build.VERSION_CODES.LOLLIPOP), packageName = "ch.dissem.apps.abit") +class AndroidNodeRegistryTest : TestBase() { + private lateinit var registry: NodeRegistry + + @Before + fun setUp() { + RuntimeEnvironment.application.deleteDatabase(SqlHelper.DATABASE_NAME) + val sqlHelper = SqlHelper(RuntimeEnvironment.application) + + registry = AndroidNodeRegistry(sqlHelper) + + registry.offerAddresses(Arrays.asList( + createAddress(1, 8444, 1, now), + createAddress(2, 8444, 1, now), + createAddress(3, 8444, 1, now), + createAddress(4, 8444, 2, now) + )) + } + + @Test + fun `ensure getKnownNodes() without streams yields empty`() { + assertThat(registry.getKnownAddresses(10), empty<NetworkAddress>()) + } + + @Test + fun `ensure predefined node is returned when database is empty`() { + RuntimeEnvironment.application.deleteDatabase(SqlHelper.DATABASE_NAME) + + val sqlHelper = SqlHelper(RuntimeEnvironment.application) + registry = AndroidNodeRegistry(sqlHelper) + + val knownAddresses = registry.getKnownAddresses(2, 1) + assertEquals(1, knownAddresses.size.toLong()) + } + + @Test + fun `ensure known addresses are retrieved`() { + var knownAddresses = registry.getKnownAddresses(2, 1) + assertEquals(2, knownAddresses.size.toLong()) + + knownAddresses = registry.getKnownAddresses(1000, 1) + assertEquals(3, knownAddresses.size.toLong()) + } + + @Test + fun `ensure offered addresses are added`() { + registry.offerAddresses(Arrays.asList( + createAddress(1, 8444, 1, now), + createAddress(10, 8444, 1, now), + createAddress(11, 8444, 1, now) + )) + + var knownAddresses = registry.getKnownAddresses(1000, 1) + assertEquals(5, knownAddresses.size.toLong()) + + registry.offerAddresses(listOf(createAddress(1, 8445, 1, now))) + + knownAddresses = registry.getKnownAddresses(1000, 1) + assertEquals(6, knownAddresses.size.toLong()) + } + + private fun createAddress(lastByte: Int, port: Int, stream: Long, time: Long): NetworkAddress { + return NetworkAddress.Builder() + .ipv6(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, lastByte) + .port(port) + .stream(stream) + .time(time) + .build() + } +} diff --git a/app/src/test/java/ch/dissem/bitmessage/repository/AndroidProofOfWorkRepositoryTest.kt b/app/src/test/java/ch/dissem/bitmessage/repository/AndroidProofOfWorkRepositoryTest.kt new file mode 100644 index 0000000..244e328 --- /dev/null +++ b/app/src/test/java/ch/dissem/bitmessage/repository/AndroidProofOfWorkRepositoryTest.kt @@ -0,0 +1,174 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.repository + +import android.os.Build.VERSION_CODES.LOLLIPOP +import ch.dissem.apps.abit.BuildConfig +import ch.dissem.apps.abit.repository.AndroidAddressRepository +import ch.dissem.apps.abit.repository.AndroidMessageRepository +import ch.dissem.apps.abit.repository.AndroidProofOfWorkRepository +import ch.dissem.apps.abit.repository.SqlHelper +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.Plaintext +import ch.dissem.bitmessage.entity.payload.GenericPayload +import ch.dissem.bitmessage.entity.payload.GetPubkey +import ch.dissem.bitmessage.entity.payload.ObjectPayload +import ch.dissem.bitmessage.entity.payload.Pubkey +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.ports.AddressRepository +import ch.dissem.bitmessage.ports.MessageRepository +import ch.dissem.bitmessage.ports.ProofOfWorkRepository +import ch.dissem.bitmessage.utils.Singleton.cryptography +import ch.dissem.bitmessage.utils.UnixTime +import org.hamcrest.CoreMatchers.* +import org.junit.Assert.assertThat +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import kotlin.properties.Delegates + +/** + * @author Christian Basler + */ +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = intArrayOf(LOLLIPOP), packageName = "ch.dissem.apps.abit") +class AndroidProofOfWorkRepositoryTest : TestBase() { + private lateinit var repo: ProofOfWorkRepository + private lateinit var addressRepo: AddressRepository + private lateinit var messageRepo: MessageRepository + + private var initialHash1: ByteArray by Delegates.notNull<ByteArray>() + private var initialHash2: ByteArray by Delegates.notNull<ByteArray>() + + @Before + fun setUp() { + RuntimeEnvironment.application.deleteDatabase(SqlHelper.DATABASE_NAME) + val sqlHelper = SqlHelper(RuntimeEnvironment.application) + + addressRepo = AndroidAddressRepository(sqlHelper) + messageRepo = AndroidMessageRepository(sqlHelper, RuntimeEnvironment.application) + repo = AndroidProofOfWorkRepository(sqlHelper) + mockedInternalContext( + addressRepository = addressRepo, + messageRepository = messageRepo, + proofOfWorkRepository = repo, + cryptography = cryptography() + ) + + repo.putObject(ObjectMessage.Builder() + .payload(GetPubkey(BitmessageAddress("BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn"))).build(), + 1000, 1000) + initialHash1 = repo.getItems()[0] + + val sender = loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8") + val recipient = loadContact() + addressRepo.save(sender) + addressRepo.save(recipient) + val plaintext = Plaintext.Builder(Plaintext.Type.MSG) + .ackData(cryptography().randomBytes(32)) + .from(sender) + .to(recipient) + .message("Subject", "Message") + .status(Plaintext.Status.DOING_PROOF_OF_WORK) + .build() + messageRepo.save(plaintext) + initialHash2 = cryptography().getInitialHash(plaintext.ackMessage!!) + repo.putObject(ProofOfWorkRepository.Item( + plaintext.ackMessage!!, + 1000, 1000, + UnixTime.now + 10 * UnixTime.MINUTE, + plaintext + )) + } + + @Test + fun `ensure object is stored`() { + val sizeBefore = repo.getItems().size + repo.putObject(ObjectMessage.Builder() + .payload(GetPubkey(BitmessageAddress("BM-2D9U2hv3YBMHM1zERP32anKfVKohyPN9x2"))).build(), + 1000, 1000) + assertThat(repo.getItems().size, `is`(sizeBefore + 1)) + } + + @Test + fun `ensure ack objects are stored`() { + val sizeBefore = repo.getItems().size + val sender = loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8") + val recipient = loadContact() + addressRepo.save(sender) + addressRepo.save(recipient) + val plaintext = Plaintext.Builder(Plaintext.Type.MSG) + .ackData(cryptography().randomBytes(32)) + .from(sender) + .to(recipient) + .message("Subject", "Message") + .status(Plaintext.Status.DOING_PROOF_OF_WORK) + .build() + messageRepo.save(plaintext) + repo.putObject(ProofOfWorkRepository.Item( + plaintext.ackMessage!!, + 1000, 1000, + UnixTime.now + 10 * UnixTime.MINUTE, + plaintext + )) + assertThat(repo.getItems().size, `is`(sizeBefore + 1)) + } + + @Test + fun `ensure item can be retrieved`() { + val item = repo.getItem(initialHash1) + assertThat(item, notNullValue()) + assertThat<ObjectPayload>(item.objectMessage.payload, instanceOf<ObjectPayload>(GetPubkey::class.java)) + assertThat(item.nonceTrialsPerByte, `is`(1000L)) + assertThat(item.extraBytes, `is`(1000L)) + } + + @Test + fun `ensure ack item can be retrieved`() { + val item = repo.getItem(initialHash2) + assertThat(item, notNullValue()) + assertThat<ObjectPayload>(item.objectMessage.payload, instanceOf<ObjectPayload>(GenericPayload::class.java)) + assertThat(item.nonceTrialsPerByte, `is`(1000L)) + assertThat(item.extraBytes, `is`(1000L)) + assertThat(item.expirationTime, not<Number>(0)) + assertThat(item.message, notNullValue()) + assertThat<PrivateKey>(item.message?.from?.privateKey, notNullValue()) + assertThat<Pubkey>(item.message?.to?.pubkey, notNullValue()) + } + + @Test(expected = RuntimeException::class) + fun `ensure retrieving nonexisting item causes exception`() { + repo.getItem(ByteArray(0)) + } + + @Test + fun `ensure item can be deleted`() { + repo.removeObject(initialHash1) + repo.removeObject(initialHash2) + assertTrue(repo.getItems().isEmpty()) + } + + @Test + fun `ensure deletion of nonexisting item is handled silently`() { + repo.removeObject(ByteArray(0)) + } +} diff --git a/app/src/test/java/ch/dissem/bitmessage/repository/TestBase.kt b/app/src/test/java/ch/dissem/bitmessage/repository/TestBase.kt new file mode 100644 index 0000000..a123618 --- /dev/null +++ b/app/src/test/java/ch/dissem/bitmessage/repository/TestBase.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.repository + +import ch.dissem.bitmessage.BitmessageContext +import ch.dissem.bitmessage.InternalContext +import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.ObjectMessage +import ch.dissem.bitmessage.entity.payload.V4Pubkey +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.entity.valueobject.PrivateKey +import ch.dissem.bitmessage.factory.Factory +import ch.dissem.bitmessage.ports.* +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.spy +import org.junit.Assert +import org.junit.BeforeClass +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.util.* + +open class TestBase { + companion object { + @BeforeClass + @JvmStatic + fun init() { + mockedInternalContext( + cryptography = SpongyCryptography(), + proofOfWorkEngine = MultiThreadedPOWEngine() + ) + } + + fun mockedInternalContext( + cryptography: Cryptography = mock {}, + inventory: Inventory = mock {}, + nodeRegistry: NodeRegistry = mock {}, + networkHandler: NetworkHandler = mock {}, + addressRepository: AddressRepository = mock {}, + messageRepository: MessageRepository = mock {}, + proofOfWorkRepository: ProofOfWorkRepository = mock {}, + proofOfWorkEngine: ProofOfWorkEngine = mock {}, + customCommandHandler: CustomCommandHandler = mock {}, + listener: BitmessageContext.Listener = mock {}, + labeler: Labeler = mock {}, + port: Int = 0, + connectionTTL: Long = 0, + connectionLimit: Int = 0 + ): InternalContext { + return spy(InternalContext( + cryptography, + inventory, + nodeRegistry, + networkHandler, + addressRepository, + messageRepository, + proofOfWorkRepository, + proofOfWorkEngine, + customCommandHandler, + listener, + labeler, + "/Jabit:TEST/", + port, + connectionTTL, + connectionLimit + )) + } + + fun randomInventoryVector(): InventoryVector { + val bytes = ByteArray(32) + RANDOM.nextBytes(bytes) + return InventoryVector(bytes) + } + + fun getResource(resourceName: String) = + TestBase::class.java.classLoader.getResourceAsStream(resourceName) + + fun loadObjectMessage(version: Int, resourceName: String): ObjectMessage { + val data = getBytes(resourceName) + val `in` = ByteArrayInputStream(data) + return Factory.getObjectMessage(version, `in`, data.size) ?: throw NoSuchElementException("error loading object message") + } + + fun getBytes(resourceName: String): ByteArray { + val `in` = getResource(resourceName) + val out = ByteArrayOutputStream() + val buffer = ByteArray(1024) + var len = `in`.read(buffer) + while (len != -1) { + out.write(buffer, 0, len) + len = `in`.read(buffer) + } + return out.toByteArray() + } + + fun loadIdentity(address: String): BitmessageAddress { + val privateKey = PrivateKey.read(getResource(address + ".privkey")) + val identity = BitmessageAddress(privateKey) + Assert.assertEquals(address, identity.address) + return identity + } + + fun loadContact(): BitmessageAddress { + val address = BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h") + val objectMessage = loadObjectMessage(3, "V4Pubkey.payload") + objectMessage.decrypt(address.publicDecryptionKey) + address.pubkey = objectMessage.payload as V4Pubkey + return address + } + + private val RANDOM = Random() + } +} diff --git a/app/src/test/resources/BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ.pubkey b/app/src/test/resources/BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ.pubkey new file mode 100644 index 0000000..39e5802 Binary files /dev/null and b/app/src/test/resources/BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ.pubkey differ diff --git a/app/src/test/resources/BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey b/app/src/test/resources/BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey new file mode 100644 index 0000000..31d210c Binary files /dev/null and b/app/src/test/resources/BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey differ diff --git a/app/src/test/resources/BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h.pubkey b/app/src/test/resources/BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h.pubkey new file mode 100644 index 0000000..fe7d2c7 Binary files /dev/null and b/app/src/test/resources/BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h.pubkey differ diff --git a/app/src/test/resources/V4Pubkey.payload b/app/src/test/resources/V4Pubkey.payload new file mode 100644 index 0000000..a5e4c5c Binary files /dev/null and b/app/src/test/resources/V4Pubkey.payload differ diff --git a/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..1f0955d --- /dev/null +++ b/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline