diff --git a/app/src/androidTest/assets/sample-db-schema-v0.realm b/app/src/androidTest/assets/sample-db-schema-v0.realm new file mode 100644 index 0000000..b65f0f2 Binary files /dev/null and b/app/src/androidTest/assets/sample-db-schema-v0.realm differ diff --git a/app/src/androidTest/java/com/codemate/koffeemate/data/local/RealmCoffeeEventRepositoryTest.kt b/app/src/androidTest/java/com/codemate/koffeemate/data/local/CoffeeEventRepositoryTest.kt similarity index 76% rename from app/src/androidTest/java/com/codemate/koffeemate/data/local/RealmCoffeeEventRepositoryTest.kt rename to app/src/androidTest/java/com/codemate/koffeemate/data/local/CoffeeEventRepositoryTest.kt index cd1f1ed..c6f07b2 100644 --- a/app/src/androidTest/java/com/codemate/koffeemate/data/local/RealmCoffeeEventRepositoryTest.kt +++ b/app/src/androidTest/java/com/codemate/koffeemate/data/local/CoffeeEventRepositoryTest.kt @@ -16,7 +16,8 @@ package com.codemate.koffeemate.data.local -import com.codemate.koffeemate.data.local.models.CoffeeBrewingEvent +import com.codemate.koffeemate.data.models.CoffeeBrewingEvent +import com.codemate.koffeemate.data.models.User import io.realm.Realm import io.realm.RealmConfiguration import org.hamcrest.core.IsEqual.equalTo @@ -24,7 +25,7 @@ import org.junit.Assert.assertThat import org.junit.Before import org.junit.Test -class RealmCoffeeEventRepositoryTest { +class CoffeeEventRepositoryTest { lateinit var coffeeEventRepository: RealmCoffeeEventRepository @Before @@ -51,9 +52,9 @@ class RealmCoffeeEventRepositoryTest { @Test fun recordBrewingEvent_WithUserId_SavesUserId() { - coffeeEventRepository.recordBrewingEvent("abc123") + coffeeEventRepository.recordBrewingEvent(User(id = "abc123")) - assertThat(coffeeEventRepository.getLastBrewingEvent()!!.userId, equalTo("abc123")) + assertThat(coffeeEventRepository.getLastBrewingEvent()!!.user!!.id, equalTo("abc123")) } @Test @@ -68,35 +69,35 @@ class RealmCoffeeEventRepositoryTest { @Test fun getLastBrewingEvent_WhenHavingAccidentsAndSuccessfulEvents_ReturnsOnlyLastBrewingEvent() { val lastSuccessfulEvent = coffeeEventRepository.recordBrewingEvent() - coffeeEventRepository.recordBrewingAccident("test") + coffeeEventRepository.recordBrewingAccident(User()) assertThat(coffeeEventRepository.getLastBrewingEvent(), equalTo(lastSuccessfulEvent)) } @Test fun getLastBrewingAccident_ReturnsLastBrewingAccident() { - val userId = "abc123" - coffeeEventRepository.recordBrewingAccident(userId) - coffeeEventRepository.recordBrewingAccident(userId) + val user = User(id = "abc123") + coffeeEventRepository.recordBrewingAccident(user) + coffeeEventRepository.recordBrewingAccident(user) - val lastAccident = coffeeEventRepository.recordBrewingAccident(userId) + val lastAccident = coffeeEventRepository.recordBrewingAccident(user) assertThat(coffeeEventRepository.getLastBrewingAccident(), equalTo(lastAccident)) } @Test fun getAccidentCountForUser_ReturnsAccidentCountForThatSpecificUser() { - val userId = "abc123" - assertThat(coffeeEventRepository.getAccidentCountForUser(userId), equalTo(0L)) + val user = User(id = "abc123") + assertThat(coffeeEventRepository.getAccidentCountForUser(user), equalTo(0L)) - coffeeEventRepository.recordBrewingAccident(userId) - coffeeEventRepository.recordBrewingAccident(userId) - coffeeEventRepository.recordBrewingAccident(userId) + coffeeEventRepository.recordBrewingAccident(user) + coffeeEventRepository.recordBrewingAccident(user) + coffeeEventRepository.recordBrewingAccident(user) - val otherUserId = "someotherid" - coffeeEventRepository.recordBrewingAccident(otherUserId) - coffeeEventRepository.recordBrewingAccident(otherUserId) + val otherUser = User(id = "someotherid") + coffeeEventRepository.recordBrewingAccident(otherUser) + coffeeEventRepository.recordBrewingAccident(otherUser) - assertThat(coffeeEventRepository.getAccidentCountForUser(userId), equalTo(3L)) + assertThat(coffeeEventRepository.getAccidentCountForUser(user), equalTo(3L)) } private fun coffeeEventCount() = diff --git a/app/src/androidTest/java/com/codemate/koffeemate/data/local/MigrationTest.kt b/app/src/androidTest/java/com/codemate/koffeemate/data/local/MigrationTest.kt new file mode 100644 index 0000000..67cb152 --- /dev/null +++ b/app/src/androidTest/java/com/codemate/koffeemate/data/local/MigrationTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2017 Codemate Ltd + * + * 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 com.codemate.koffeemate.data.local + +import android.content.Context +import android.support.test.InstrumentationRegistry +import com.codemate.koffeemate.data.models.CoffeeBrewingEvent +import io.realm.Realm +import io.realm.RealmConfiguration +import org.hamcrest.core.IsEqual.equalTo +import org.hamcrest.core.IsNull.nullValue +import org.junit.Assert.assertThat +import org.junit.Before +import org.junit.Test +import java.io.File +import java.io.IOException + +class MigrationTest { + lateinit var context: Context + + @Before + fun setUp() { + context = InstrumentationRegistry.getContext() + } + + /************************************************************* + * Migration tests from schema version 0 to 1 + *************************************************************/ + @Test + fun testMigrationFromVersionZeroToOne() { + val config = RealmConfiguration.Builder() + .name("migration-test.realm") + .schemaVersion(1) + .migration(Migration()) + .build() + + // The "sample-db-schema-v0.realm" contains three sample records, + // in the old database schema, which used userIds instead of User + // objects. + copyRealmFromAssets(context, "sample-db-schema-v0.realm", config) + val realm = Realm.getInstance(config) + + val all = realm.where(CoffeeBrewingEvent::class.java).findAll() + assertThat(all.size, equalTo(3)) + + val brewingEventWithoutUserId = all[0] + assertThat(brewingEventWithoutUserId.id, equalTo("adf9c9b9-e521-462f-9d67-ff2a11d7b62c")) + assertThat(brewingEventWithoutUserId.time, equalTo(1485872637115L)) + assertThat(brewingEventWithoutUserId.isSuccessful, equalTo(true)) + assertThat(brewingEventWithoutUserId.user, nullValue()) + + val brewingEventWithUserId = all[1] + assertThat(brewingEventWithUserId.id, equalTo("0e742762-7181-4bc0-b7b5-d1ff68991dd6")) + assertThat(brewingEventWithUserId.time, equalTo(1485872637117L)) + assertThat(brewingEventWithUserId.isSuccessful, equalTo(true)) + assertThat(brewingEventWithUserId.user!!.id, equalTo("abc-123")) + assertThat(brewingEventWithUserId.user!!.last_updated, equalTo(0L)) + + val brewingAccident = all[2] + assertThat(brewingAccident.id, equalTo("480bb3b9-a01f-45cb-87cd-113465d4038a")) + assertThat(brewingAccident.time, equalTo(1485872637118L)) + assertThat(brewingAccident.isSuccessful, equalTo(false)) + assertThat(brewingAccident.user!!.id, equalTo("abc-123")) + assertThat(brewingEventWithUserId.user!!.last_updated, equalTo(0L)) + + // Make sure we don't generate different timestamps for the users in + // this new schema. + assertThat(brewingEventWithUserId.user!!.last_updated, equalTo(brewingAccident.user!!.last_updated)) + + realm.close() + } + + @Throws(IOException::class) + fun copyRealmFromAssets(context: Context, realmPath: String, config: RealmConfiguration) { + Realm.deleteRealm(config) + + context.assets.open(realmPath).use { inputStream -> + val outFile = File(config.realmDirectory, config.realmFileName) + + outFile.outputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } + } + } +} diff --git a/app/src/androidTest/java/com/codemate/koffeemate/data/local/UserRepositoryTest.kt b/app/src/androidTest/java/com/codemate/koffeemate/data/local/UserRepositoryTest.kt new file mode 100644 index 0000000..33ccdbc --- /dev/null +++ b/app/src/androidTest/java/com/codemate/koffeemate/data/local/UserRepositoryTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2017 Codemate Ltd + * + * 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 com.codemate.koffeemate.data.local + +import com.codemate.koffeemate.data.models.User +import io.realm.Realm +import io.realm.RealmConfiguration +import org.hamcrest.core.IsEqual.equalTo +import org.junit.Assert.assertThat +import org.junit.Before +import org.junit.Test + +class UserRepositoryTest { + val TEST_USERS_UNIQUE = listOf(User(id = "abc123"), User(id = "123abc"), User(id = "a1b2c3")) + val TEST_USERS_DUPLICATE = listOf(User(id = "abc123"), User(id = "abc123"), User(id = "abc123")) + + lateinit var userRepository: UserRepository + + @Before + fun setUp() { + val realmConfig = RealmConfiguration.Builder() + .name("test.realm") + .inMemory() + .build() + + Realm.setDefaultConfiguration(realmConfig) + Realm.getDefaultInstance().executeTransaction(Realm::deleteAll) + + userRepository = RealmUserRepository() + } + + @Test + fun addAll_WhenUsersAreUnique_PersistsAllInDatabase() { + userRepository.addAll(TEST_USERS_UNIQUE) + + val all = userRepository.getAll() + assertThat(all[0].id, equalTo(TEST_USERS_UNIQUE[0].id)) + assertThat(all[1].id, equalTo(TEST_USERS_UNIQUE[1].id)) + assertThat(all[2].id, equalTo(TEST_USERS_UNIQUE[2].id)) + } + + @Test + fun addAll_WhenUsersAreDuplicate_PersistsOnlyOne() { + userRepository.addAll(TEST_USERS_DUPLICATE) + + val all = userRepository.getAll() + assertThat(all.size, equalTo(1)) + } + + @Test + fun addAll_WhenTryingToAddExistingUser_UpdatesIt() { + userRepository.addAll(listOf(User(id = "abc123", name = "John Smith"))) + assertThat(userRepository.getAll().first().name, equalTo("John Smith")) + + userRepository.addAll(listOf(User(id = "abc123", name = "Kevin Doe"))) + + val all = userRepository.getAll() + assertThat(all.size, equalTo(1)) + assertThat(all.first().name, equalTo("Kevin Doe")) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1b342fd..fdb0e95 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,10 +21,6 @@ - - diff --git a/app/src/main/java/com/codemate/koffeemate/KoffeemateApp.kt b/app/src/main/java/com/codemate/koffeemate/KoffeemateApp.kt index 9e27e83..80b68e9 100644 --- a/app/src/main/java/com/codemate/koffeemate/KoffeemateApp.kt +++ b/app/src/main/java/com/codemate/koffeemate/KoffeemateApp.kt @@ -1,12 +1,14 @@ package com.codemate.koffeemate import android.app.Application +import com.codemate.koffeemate.data.local.Migration import com.codemate.koffeemate.data.network.SlackApi import com.codemate.koffeemate.di.components.AppComponent import com.codemate.koffeemate.di.components.DaggerAppComponent import com.codemate.koffeemate.di.modules.AppModule import com.codemate.koffeemate.di.modules.NetModule import io.realm.Realm +import io.realm.RealmConfiguration class KoffeemateApp : Application() { companion object { @@ -16,10 +18,21 @@ class KoffeemateApp : Application() { override fun onCreate() { super.onCreate() - Realm.init(this) + initializeRealm() + appComponent = DaggerAppComponent.builder() .appModule(AppModule(this)) .netModule(NetModule(SlackApi.BASE_URL)) .build() } + + private fun initializeRealm() { + Realm.init(this) + + val configuration = RealmConfiguration.Builder() + .migration(Migration()) + .schemaVersion(1) + .build() + Realm.setDefaultConfiguration(configuration) + } } \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/data/local/CoffeeEventRepository.kt b/app/src/main/java/com/codemate/koffeemate/data/local/CoffeeEventRepository.kt index 3ad2100..7251845 100644 --- a/app/src/main/java/com/codemate/koffeemate/data/local/CoffeeEventRepository.kt +++ b/app/src/main/java/com/codemate/koffeemate/data/local/CoffeeEventRepository.kt @@ -1,11 +1,82 @@ package com.codemate.koffeemate.data.local -import com.codemate.koffeemate.data.local.models.CoffeeBrewingEvent +import com.codemate.koffeemate.data.models.CoffeeBrewingEvent +import com.codemate.koffeemate.data.models.User +import io.realm.Realm +import io.realm.Sort +import java.util.* interface CoffeeEventRepository { - fun recordBrewingEvent(userId: String? = ""): CoffeeBrewingEvent - fun recordBrewingAccident(userId: String): CoffeeBrewingEvent - fun getAccidentCountForUser(userId: String): Long + fun recordBrewingEvent(user: User? = null): CoffeeBrewingEvent + fun recordBrewingAccident(user: User): CoffeeBrewingEvent + fun getAccidentCountForUser(user: User): Long + fun getLastBrewingEvent(): CoffeeBrewingEvent? fun getLastBrewingAccident(): CoffeeBrewingEvent? +} + +class RealmCoffeeEventRepository : CoffeeEventRepository { + override fun recordBrewingEvent(user: User?) = with(Realm.getDefaultInstance()) { + var event: CoffeeBrewingEvent? = null + executeTransaction { + event = newEvent(it).apply { + time = System.currentTimeMillis() + isSuccessful = true + this.user = if (user != null) copyToRealmOrUpdate(user) else null + } + } + + close() + return@with event!! + } + + override fun recordBrewingAccident(user: User) = with(Realm.getDefaultInstance()) { + var event: CoffeeBrewingEvent? = null + executeTransaction { + event = newEvent(it).apply { + time = System.currentTimeMillis() + isSuccessful = false + this.user = copyToRealmOrUpdate(user) + } + } + + close() + return@with event!! + } + + override fun getAccidentCountForUser(user: User) = with(Realm.getDefaultInstance()) { + val count = where(CoffeeBrewingEvent::class.java) + .equalTo("isSuccessful", false) + .equalTo("user.id", user.id) + .count() + + close() + return@with count + } + + override fun getLastBrewingEvent() = with(Realm.getDefaultInstance()) { + val lastEvent = where(CoffeeBrewingEvent::class.java) + .equalTo("isSuccessful", true) + .findAllSorted("time", Sort.ASCENDING) + .lastOrNull() + + close() + return@with lastEvent + } + + override fun getLastBrewingAccident() = with(Realm.getDefaultInstance()) { + val lastAccident = where(CoffeeBrewingEvent::class.java) + .equalTo("isSuccessful", false) + .findAllSorted("time", Sort.ASCENDING) + .lastOrNull() + + close() + return@with lastAccident + } + + private fun newEvent(realm: Realm) = + realm.createObject( + CoffeeBrewingEvent::class.java, + UUID.randomUUID().toString() + ) } \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/data/local/Migration.kt b/app/src/main/java/com/codemate/koffeemate/data/local/Migration.kt new file mode 100644 index 0000000..a7045e7 --- /dev/null +++ b/app/src/main/java/com/codemate/koffeemate/data/local/Migration.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2017 Codemate Ltd + * + * 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 com.codemate.koffeemate.data.local + +import io.realm.DynamicRealm +import io.realm.DynamicRealmObject +import io.realm.FieldAttribute +import io.realm.RealmMigration +import io.realm.exceptions.RealmPrimaryKeyConstraintException + +class Migration : RealmMigration { + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + val schema = realm.schema + + /************************************************************* + // Version 0 + class CoffeeBrewingEvent + @PrimaryKey + id: String + time: Long + isSuccessful: Boolean + userId: String + + // Version 1 + Changed Tables: + class CoffeeBrewingEvent + @PrimaryKey + id: String + time: Long + isSuccessful: Boolean + user: User + + New Tables: + class User + @PrimaryKey + id: String + name: String + profile: Profile + real_name: String + is_bot: Boolean + deleted: Boolean + last_updated: Long + + class Profile + first_name: String + last_name: String + real_name: String + image_72: String + image_192: String + image_512: String + *************************************************************/ + if (oldVersion == 0L) { + val profileSchema = schema.create("Profile") + .addField("first_name", String::class.java) + .addField("last_name", String::class.java) + .addField("real_name", String::class.java) + .addField("image_72", String::class.java) + .addField("image_192", String::class.java) + .addField("image_512", String::class.java) + + val userSchema = schema.create("User") + .addField("id", String::class.java, FieldAttribute.PRIMARY_KEY) + .addField("name", String::class.java) + .addRealmObjectField("profile", profileSchema) + .addField("real_name", String::class.java) + .addField("is_bot", Boolean::class.java) + .addField("deleted", Boolean::class.java) + .addField("last_updated", Long::class.java) + + schema.get("CoffeeBrewingEvent") + .addRealmObjectField("user", userSchema) + .transform { brewingEvent -> + brewingEvent.getString("userId")?.let { previousUserId -> + if (previousUserId.isNotBlank()) { + var user: DynamicRealmObject? + + // DynamicRealm doesn't allow us create duplicate User objects, + // since the id field is a primary key. insertOrUpdate() and similar + // methods are unavailable when using DynamicRealms. + // + // Try-catch is the only way to handle the migration of userIds to + // users in this case. + try { + user = realm.createObject("User", previousUserId) + } catch (e: RealmPrimaryKeyConstraintException) { + user = realm.where("User") + .equalTo("id", previousUserId) + .findFirst() + } + + brewingEvent.setObject("user", user) + } + } + } + .removeField("userId") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/data/local/RealmCoffeeEventRepository.kt b/app/src/main/java/com/codemate/koffeemate/data/local/RealmCoffeeEventRepository.kt deleted file mode 100644 index a0848a6..0000000 --- a/app/src/main/java/com/codemate/koffeemate/data/local/RealmCoffeeEventRepository.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.codemate.koffeemate.data.local - -import com.codemate.koffeemate.data.local.models.CoffeeBrewingEvent -import io.realm.Realm -import io.realm.Sort -import java.util.* - -class RealmCoffeeEventRepository : CoffeeEventRepository { - override fun recordBrewingAccident(userId: String) = with(Realm.getDefaultInstance()) { - var event: CoffeeBrewingEvent? = null - executeTransaction { - event = newEvent(it).apply { - time = System.currentTimeMillis() - isSuccessful = false - this.userId = userId - } - } - - close() - return@with event!! - } - - override fun recordBrewingEvent(userId: String?) = with(Realm.getDefaultInstance()) { - var event: CoffeeBrewingEvent? = null - executeTransaction { - event = newEvent(it).apply { - time = System.currentTimeMillis() - isSuccessful = true - this.userId = userId ?: "" - } - } - - close() - return@with event!! - } - - override fun getAccidentCountForUser(userId: String) = - Realm.getDefaultInstance() - .where(CoffeeBrewingEvent::class.java) - .equalTo("isSuccessful", false) - .equalTo("userId", userId) - .count() - - override fun getLastBrewingEvent(): CoffeeBrewingEvent? { - return Realm.getDefaultInstance() - .where(CoffeeBrewingEvent::class.java) - .equalTo("isSuccessful", true) - .findAllSorted("time", Sort.ASCENDING) - .lastOrNull() - } - - override fun getLastBrewingAccident(): CoffeeBrewingEvent? { - return Realm.getDefaultInstance() - .where(CoffeeBrewingEvent::class.java) - .equalTo("isSuccessful", false) - .findAllSorted("time", Sort.ASCENDING) - .lastOrNull() - } - - private fun newEvent(realm: Realm) = - realm.createObject( - CoffeeBrewingEvent::class.java, - UUID.randomUUID().toString() - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/data/local/UserRepository.kt b/app/src/main/java/com/codemate/koffeemate/data/local/UserRepository.kt new file mode 100644 index 0000000..79c7862 --- /dev/null +++ b/app/src/main/java/com/codemate/koffeemate/data/local/UserRepository.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2017 Codemate Ltd + * + * 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 com.codemate.koffeemate.data.local + +import com.codemate.koffeemate.data.models.User +import io.realm.Realm + +interface UserRepository { + fun addAll(users: List) + fun getAll(): List +} + +class RealmUserRepository : UserRepository { + override fun addAll(users: List) { + with(Realm.getDefaultInstance()) { + executeTransaction { copyToRealmOrUpdate(users) } + close() + } + } + + override fun getAll(): List = with(Realm.getDefaultInstance()) { + val all = where(User::class.java).findAll() + val copy = copyFromRealm(all) + + close() + return@with copy + } +} \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/data/local/models/CoffeeBrewingEvent.kt b/app/src/main/java/com/codemate/koffeemate/data/models/CoffeeBrewingEvent.kt similarity index 74% rename from app/src/main/java/com/codemate/koffeemate/data/local/models/CoffeeBrewingEvent.kt rename to app/src/main/java/com/codemate/koffeemate/data/models/CoffeeBrewingEvent.kt index 58a363b..e18f0bb 100644 --- a/app/src/main/java/com/codemate/koffeemate/data/local/models/CoffeeBrewingEvent.kt +++ b/app/src/main/java/com/codemate/koffeemate/data/models/CoffeeBrewingEvent.kt @@ -1,4 +1,4 @@ -package com.codemate.koffeemate.data.local.models +package com.codemate.koffeemate.data.models import io.realm.RealmObject import io.realm.annotations.PrimaryKey @@ -8,5 +8,5 @@ open class CoffeeBrewingEvent( open var id: String = "", open var time: Long = 0, open var isSuccessful: Boolean = false, - open var userId: String = "" + open var user: User? = null ) : RealmObject() \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/data/models/Profile.kt b/app/src/main/java/com/codemate/koffeemate/data/models/Profile.kt new file mode 100644 index 0000000..0329c3b --- /dev/null +++ b/app/src/main/java/com/codemate/koffeemate/data/models/Profile.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Codemate Ltd + * + * 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 com.codemate.koffeemate.data.models + +import io.realm.RealmObject + +open class Profile( + open var first_name: String = "", + open var last_name: String = "", + open var real_name: String = "", + open var image_72: String? = null, + open var image_192: String? = null, + open var image_512: String? = null +) : RealmObject() { + val largestAvailableImage: String + get() = image_512 ?: image_192 ?: image_72 ?: "" + + val smallestAvailableImage: String + get() = image_72 ?: image_192 ?: image_512 ?: "" +} diff --git a/app/src/main/java/com/codemate/koffeemate/data/models/User.kt b/app/src/main/java/com/codemate/koffeemate/data/models/User.kt new file mode 100644 index 0000000..1bc81d9 --- /dev/null +++ b/app/src/main/java/com/codemate/koffeemate/data/models/User.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Codemate Ltd + * + * 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 com.codemate.koffeemate.data.models + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +class UserListResponse { + var members = listOf() +} + +fun List.isFreshEnough(maxStaleness: Long): Boolean { + val oldestAcceptedTimestamp = System.currentTimeMillis() - maxStaleness + val sorted = sortedByDescending(User::last_updated) + val freshestUser = sorted.first() + + return freshestUser.last_updated > oldestAcceptedTimestamp +} + +open class User( + @PrimaryKey + open var id: String = "", + open var name: String = "", + open var profile: Profile = Profile(), + open var real_name: String? = null, + open var is_bot: Boolean = false, + open var deleted: Boolean = false, + open var last_updated: Long = 0 +) : RealmObject() \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/data/network/SlackApi.kt b/app/src/main/java/com/codemate/koffeemate/data/network/SlackApi.kt index 9587d4a..91803e9 100644 --- a/app/src/main/java/com/codemate/koffeemate/data/network/SlackApi.kt +++ b/app/src/main/java/com/codemate/koffeemate/data/network/SlackApi.kt @@ -1,14 +1,16 @@ package com.codemate.koffeemate.data.network import com.codemate.koffeemate.BuildConfig -import com.codemate.koffeemate.data.network.models.UserListResponse +import com.codemate.koffeemate.data.models.UserListResponse import com.codemate.koffeemate.extensions.toRequestBody import okhttp3.HttpUrl import okhttp3.MultipartBody import okhttp3.RequestBody import okhttp3.ResponseBody -import retrofit2.Call import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.* import rx.Observable @@ -40,5 +42,15 @@ interface SlackApi { companion object { val BASE_URL = HttpUrl.parse("https://slack.com/api/")!! + + fun create(baseUrl: HttpUrl): SlackApi { + val retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .build() + + return retrofit.create(SlackApi::class.java) + } } } diff --git a/app/src/main/java/com/codemate/koffeemate/data/network/SlackService.kt b/app/src/main/java/com/codemate/koffeemate/data/network/SlackService.kt deleted file mode 100644 index 7302767..0000000 --- a/app/src/main/java/com/codemate/koffeemate/data/network/SlackService.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016 Codemate Ltd - * - * 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 com.codemate.koffeemate.data.network - -import okhttp3.HttpUrl -import retrofit2.Retrofit -import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory -import retrofit2.converter.gson.GsonConverterFactory - -object SlackService { - fun getApi(baseUrl: HttpUrl): SlackApi { - val retrofit = Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) - .build() - - return retrofit.create(SlackApi::class.java) - } -} diff --git a/app/src/main/java/com/codemate/koffeemate/data/network/models/Profile.kt b/app/src/main/java/com/codemate/koffeemate/data/network/models/Profile.kt deleted file mode 100644 index 802c471..0000000 --- a/app/src/main/java/com/codemate/koffeemate/data/network/models/Profile.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.codemate.koffeemate.data.network.models - -class Profile { - lateinit var first_name: String - lateinit var last_name: String - lateinit var real_name: String - - var image_72: String? = null - var image_192: String? = null - var image_512: String? = null - - val largestAvailableImage: String - get() { - var imageUrl: String? = image_512 - - if (imageUrl.isNullOrBlank()) { - imageUrl = image_192 - } - - if (imageUrl.isNullOrBlank()) { - imageUrl = image_72 - } - - return imageUrl ?: "" - } - - val smallestAvailableImage: String - get() { - var imageUrl: String? = image_72 - - if (imageUrl.isNullOrBlank()) { - imageUrl = image_192 - } - - if (imageUrl.isNullOrBlank()) { - imageUrl = image_512 - } - - return imageUrl ?: "" - } -} diff --git a/app/src/main/java/com/codemate/koffeemate/data/network/models/User.kt b/app/src/main/java/com/codemate/koffeemate/data/network/models/User.kt deleted file mode 100644 index fe38840..0000000 --- a/app/src/main/java/com/codemate/koffeemate/data/network/models/User.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.codemate.koffeemate.data.network.models - -class UserListResponse { - var members = listOf() -} - -class User { - lateinit var id: String - lateinit var name: String - lateinit var profile: Profile - - var real_name: String? = null - var is_bot: Boolean = false - var deleted: Boolean = false -} diff --git a/app/src/main/java/com/codemate/koffeemate/di/components/AppComponent.kt b/app/src/main/java/com/codemate/koffeemate/di/components/AppComponent.kt index 50bb9b4..63626a9 100644 --- a/app/src/main/java/com/codemate/koffeemate/di/components/AppComponent.kt +++ b/app/src/main/java/com/codemate/koffeemate/di/components/AppComponent.kt @@ -16,11 +16,7 @@ package com.codemate.koffeemate.di.components -import com.codemate.koffeemate.di.modules.ActivityModule -import com.codemate.koffeemate.di.modules.AppModule -import com.codemate.koffeemate.di.modules.NetModule -import com.codemate.koffeemate.di.modules.PersistenceModule -import com.codemate.koffeemate.ui.userselector.UserSelectorActivity +import com.codemate.koffeemate.di.modules.* import com.codemate.koffeemate.ui.userselector.UserSelectorFragment import dagger.Component import javax.inject.Singleton @@ -29,7 +25,8 @@ import javax.inject.Singleton @Component(modules = arrayOf( AppModule::class, PersistenceModule::class, - NetModule::class) + NetModule::class, + ThreadingModule::class) ) interface AppComponent { fun inject(userSelectorFragment: UserSelectorFragment) diff --git a/app/src/main/java/com/codemate/koffeemate/di/modules/AppModule.kt b/app/src/main/java/com/codemate/koffeemate/di/modules/AppModule.kt index c4d66fa..d25e57f 100644 --- a/app/src/main/java/com/codemate/koffeemate/di/modules/AppModule.kt +++ b/app/src/main/java/com/codemate/koffeemate/di/modules/AppModule.kt @@ -21,16 +21,8 @@ import com.codemate.koffeemate.KoffeemateApp import com.codemate.koffeemate.common.AndroidAwardBadgeCreator import com.codemate.koffeemate.common.AwardBadgeCreator import com.codemate.koffeemate.common.BrewingProgressUpdater -import com.codemate.koffeemate.data.local.CoffeeEventRepository -import com.codemate.koffeemate.data.local.CoffeePreferences -import com.codemate.koffeemate.data.network.SlackApi -import com.codemate.koffeemate.ui.userselector.LoadUsersUseCase -import com.codemate.koffeemate.ui.main.PostAccidentUseCase -import com.codemate.koffeemate.ui.main.SendCoffeeAnnouncementUseCase import dagger.Module import dagger.Provides -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers import java.util.concurrent.TimeUnit import javax.inject.Singleton @@ -51,41 +43,4 @@ class AppModule(val app: KoffeemateApp) { @Provides @Singleton fun provideAwardBadgeCreator(ctx: Context): AwardBadgeCreator = AndroidAwardBadgeCreator(ctx) - - /** - * Move these to a better place once you actually understand Dagger ¯\_(ツ)_/¯ - */ - @Provides - @Singleton - fun provideSendCoffeeAnnouncementUseCase(slackApi: SlackApi) = - SendCoffeeAnnouncementUseCase( - slackApi, - Schedulers.newThread(), - AndroidSchedulers.mainThread() - ) - - @Provides - @Singleton - fun provideLoadUsersUseCase(slackApi: SlackApi) = - LoadUsersUseCase( - slackApi, - Schedulers.newThread(), - AndroidSchedulers.mainThread() - ) - - @Provides - @Singleton - fun providePostAccidentUseCase( - slackApi: SlackApi, - coffeeEventRepository: CoffeeEventRepository, - coffeePreferences: CoffeePreferences, - awardBadgeCreator: AwardBadgeCreator - ) = PostAccidentUseCase( - slackApi, - coffeeEventRepository, - coffeePreferences, - awardBadgeCreator, - Schedulers.newThread(), - AndroidSchedulers.mainThread() - ) } \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/di/modules/NetModule.kt b/app/src/main/java/com/codemate/koffeemate/di/modules/NetModule.kt index 4b86b84..0a70494 100644 --- a/app/src/main/java/com/codemate/koffeemate/di/modules/NetModule.kt +++ b/app/src/main/java/com/codemate/koffeemate/di/modules/NetModule.kt @@ -16,7 +16,7 @@ package com.codemate.koffeemate.di.modules -import com.codemate.koffeemate.data.network.SlackService +import com.codemate.koffeemate.data.network.SlackApi import dagger.Module import dagger.Provides import okhttp3.HttpUrl @@ -26,5 +26,5 @@ import javax.inject.Singleton class NetModule(val baseUrl: HttpUrl) { @Provides @Singleton - fun provideApi() = SlackService.getApi(baseUrl) + fun provideApi() = SlackApi.create(baseUrl) } \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/di/modules/PersistenceModule.kt b/app/src/main/java/com/codemate/koffeemate/di/modules/PersistenceModule.kt index b0ea703..dd4431d 100644 --- a/app/src/main/java/com/codemate/koffeemate/di/modules/PersistenceModule.kt +++ b/app/src/main/java/com/codemate/koffeemate/di/modules/PersistenceModule.kt @@ -17,9 +17,7 @@ package com.codemate.koffeemate.di.modules import android.content.Context -import com.codemate.koffeemate.data.local.CoffeeEventRepository -import com.codemate.koffeemate.data.local.CoffeePreferences -import com.codemate.koffeemate.data.local.RealmCoffeeEventRepository +import com.codemate.koffeemate.data.local.* import dagger.Module import dagger.Provides import javax.inject.Singleton @@ -33,4 +31,8 @@ class PersistenceModule { @Provides @Singleton fun provideCoffeeEventRepository(): CoffeeEventRepository = RealmCoffeeEventRepository() + + @Provides + @Singleton + fun provideUserRepository(): UserRepository = RealmUserRepository() } \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/di/modules/ThreadingModule.kt b/app/src/main/java/com/codemate/koffeemate/di/modules/ThreadingModule.kt new file mode 100644 index 0000000..7e11cfb --- /dev/null +++ b/app/src/main/java/com/codemate/koffeemate/di/modules/ThreadingModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Codemate Ltd + * + * 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 com.codemate.koffeemate.di.modules + +import dagger.Module +import dagger.Provides +import rx.Scheduler +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import javax.inject.Named + +@Module +class ThreadingModule { + @Provides + @Named("subscriber") + fun provideSubscriber(): Scheduler = Schedulers.newThread() + + @Provides + @Named("observer") + fun provideObserver(): Scheduler = AndroidSchedulers.mainThread() +} \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/ui/main/MainActivity.kt b/app/src/main/java/com/codemate/koffeemate/ui/main/MainActivity.kt index b1ff92d..4e53164 100644 --- a/app/src/main/java/com/codemate/koffeemate/ui/main/MainActivity.kt +++ b/app/src/main/java/com/codemate/koffeemate/ui/main/MainActivity.kt @@ -1,7 +1,6 @@ package com.codemate.koffeemate.ui.main import android.app.ProgressDialog -import android.content.Intent import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.view.View @@ -10,12 +9,11 @@ import com.bumptech.glide.Glide import com.codemate.koffeemate.KoffeemateApp import com.codemate.koffeemate.R import com.codemate.koffeemate.common.ScreenSaver -import com.codemate.koffeemate.data.local.models.CoffeeBrewingEvent -import com.codemate.koffeemate.data.network.models.User +import com.codemate.koffeemate.data.models.CoffeeBrewingEvent +import com.codemate.koffeemate.data.models.User import com.codemate.koffeemate.di.modules.ActivityModule import com.codemate.koffeemate.extensions.loadBitmap import com.codemate.koffeemate.ui.settings.SettingsActivity -import com.codemate.koffeemate.ui.userselector.UserSelectorActivity import com.codemate.koffeemate.ui.userselector.UserSelectorFragment import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.view_coffee_progress.view.* @@ -23,7 +21,8 @@ import org.jetbrains.anko.* import javax.inject.Inject class MainActivity : AppCompatActivity(), MainView, UserSelectorFragment.UserSelectListener { - private val REQUEST_CODE_SHAME_USER = 1 + private val REQUEST_WHOS_BREWING = 1 + private val REQUEST_WHO_FAILED_BREWING = 2 @Inject lateinit var presenter: MainPresenter @@ -91,20 +90,28 @@ class MainActivity : AppCompatActivity(), MainView, UserSelectorFragment.UserSel // Functions for identifying who brews the coffee override fun selectCoffeeBrewingPerson() { - UserSelectorFragment - .newInstance() - .show(supportFragmentManager, "user_selector") + UserSelectorFragment.newInstance( + title = getString(R.string.prompt_select_person_below), + requestCode = REQUEST_WHOS_BREWING + ).show(supportFragmentManager, "user_selector") } override fun clearCoffeeBrewingPerson() { coffeeProgressView.userSetterButton.clearUser() } - override fun onUserSelected(user: User) { - Glide.with(this) - .load(user.profile.smallestAvailableImage) - .into(coffeeProgressView.userSetterButton) - presenter.personBrewingCoffee = user + override fun onUserSelected(user: User, requestCode: Int) { + when (requestCode) { + REQUEST_WHOS_BREWING -> { + Glide.with(this) + .load(user.profile.smallestAvailableImage) + .into(coffeeProgressView.userSetterButton) + presenter.personBrewingCoffee = user + } + REQUEST_WHO_FAILED_BREWING -> { + showPostAccidentAnnouncementPrompt(user) + } + } } // MainView methods --> @@ -156,35 +163,24 @@ class MainActivity : AppCompatActivity(), MainView, UserSelectorFragment.UserSel // Shaming users for coffee brewing failures --> override fun launchUserSelector() { - startActivityForResult( - intentFor(), - REQUEST_CODE_SHAME_USER - ) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == REQUEST_CODE_SHAME_USER && resultCode == RESULT_OK && data != null) { - val userId = data.getStringExtra(UserSelectorActivity.RESULT_USER_ID) - val fullName = data.getStringExtra(UserSelectorActivity.RESULT_USER_FULL_NAME) - val firstName = data.getStringExtra(UserSelectorActivity.RESULT_USER_FIRST_NAME) - val largestProfilePicUrl = data.getStringExtra(UserSelectorActivity.RESULT_USER_PROFILE_LARGEST_PIC_URL) - - showPostAccidentAnnouncementPrompt(userId, fullName, firstName, largestProfilePicUrl) - } + UserSelectorFragment.newInstance( + title = getString(R.string.prompt_who_is_guilty), + requestCode = REQUEST_WHO_FAILED_BREWING + ).show(supportFragmentManager, "user_selector") } - override fun showPostAccidentAnnouncementPrompt(userId: String, fullName: String, firstName: String, largestProfilePicUrl: String) { + override fun showPostAccidentAnnouncementPrompt(user: User) { alert { title(R.string.prompt_reset_the_counter) - message(getString(R.string.message_posting_to_slack_fmt, fullName)) + message(getString(R.string.message_posting_to_slack_fmt, user.profile.real_name)) negativeButton(R.string.action_cancel) positiveButton(R.string.action_announce_coffee_accident) { accidentProgress = indeterminateProgressDialog(R.string.progress_message_shaming_person_on_slack) - val comment = getString(R.string.message_congratulations_to_user_fmt, firstName) + val comment = getString(R.string.message_congratulations_to_user_fmt, user.profile.first_name) - Glide.with(this@MainActivity).loadBitmap(largestProfilePicUrl) { profilePic -> - presenter.announceCoffeeBrewingAccident(comment, userId, firstName, profilePic) + Glide.with(this@MainActivity).loadBitmap(user.profile.largestAvailableImage) { profilePic -> + presenter.announceCoffeeBrewingAccident(comment, user, profilePic) } } }.show() diff --git a/app/src/main/java/com/codemate/koffeemate/ui/main/MainPresenter.kt b/app/src/main/java/com/codemate/koffeemate/ui/main/MainPresenter.kt index 545723c..8582ccd 100644 --- a/app/src/main/java/com/codemate/koffeemate/ui/main/MainPresenter.kt +++ b/app/src/main/java/com/codemate/koffeemate/ui/main/MainPresenter.kt @@ -5,8 +5,10 @@ import com.codemate.koffeemate.common.BrewingProgressUpdater import com.codemate.koffeemate.common.ScreenSaver import com.codemate.koffeemate.data.local.CoffeeEventRepository import com.codemate.koffeemate.data.local.CoffeePreferences -import com.codemate.koffeemate.data.network.models.User +import com.codemate.koffeemate.data.models.User import com.codemate.koffeemate.ui.base.BasePresenter +import com.codemate.koffeemate.usecases.PostAccidentUseCase +import com.codemate.koffeemate.usecases.SendCoffeeAnnouncementUseCase import okhttp3.ResponseBody import retrofit2.Response import rx.Subscriber @@ -58,7 +60,7 @@ class MainPresenter @Inject constructor( getView()?.updateCoffeeProgress(0) getView()?.resetCoffeeViewStatus() - coffeeEventRepository.recordBrewingEvent(personBrewingCoffee?.id) + coffeeEventRepository.recordBrewingEvent(personBrewingCoffee) updateLastBrewingEventTime() } @@ -107,13 +109,7 @@ class MainPresenter @Inject constructor( if (coffeePreferences.isAccidentChannelSet()) { personBrewingCoffee?.let { - getView()?.showPostAccidentAnnouncementPrompt( - it.id, - it.profile.real_name, - it.profile.first_name, - it.profile.largestAvailableImage - ) - + getView()?.showPostAccidentAnnouncementPrompt(it) return } @@ -123,10 +119,10 @@ class MainPresenter @Inject constructor( } } - fun announceCoffeeBrewingAccident(comment: String, userId: String, userName: String, profilePic: Bitmap) { + fun announceCoffeeBrewingAccident(comment: String, user: User, profilePic: Bitmap) { ensureViewIsAttached() - postAccidentUseCase.execute(comment, userId, userName, profilePic).subscribe( + postAccidentUseCase.execute(comment, user, profilePic).subscribe( object : Subscriber>() { override fun onNext(response: Response) { getView()?.showAccidentPostedSuccessfullyMessage() diff --git a/app/src/main/java/com/codemate/koffeemate/ui/main/MainView.kt b/app/src/main/java/com/codemate/koffeemate/ui/main/MainView.kt index 8ace990..803977e 100644 --- a/app/src/main/java/com/codemate/koffeemate/ui/main/MainView.kt +++ b/app/src/main/java/com/codemate/koffeemate/ui/main/MainView.kt @@ -1,6 +1,7 @@ package com.codemate.koffeemate.ui.main -import com.codemate.koffeemate.data.local.models.CoffeeBrewingEvent +import com.codemate.koffeemate.data.models.CoffeeBrewingEvent +import com.codemate.koffeemate.data.models.User import com.codemate.koffeemate.ui.base.MvpView interface MainView : MvpView { @@ -18,12 +19,7 @@ interface MainView : MvpView { fun clearCoffeeBrewingPerson() fun launchUserSelector() - fun showPostAccidentAnnouncementPrompt( - userId: String, - fullName: String, - firstName: String, - largestProfilePicUrl: String - ) + fun showPostAccidentAnnouncementPrompt(user: User) fun showAccidentPostedSuccessfullyMessage() fun showErrorPostingAccidentMessage() } \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorActivity.kt b/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorActivity.kt deleted file mode 100644 index aeafefa..0000000 --- a/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorActivity.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.codemate.koffeemate.ui.userselector - -import android.content.Intent -import android.os.Bundle -import android.support.v7.app.AppCompatActivity -import android.view.MenuItem -import com.codemate.koffeemate.R -import com.codemate.koffeemate.data.network.models.User - -class UserSelectorActivity : AppCompatActivity(), UserSelectorFragment.UserSelectListener { - companion object { - val RESULT_USER_ID = "user_id" - val RESULT_USER_FULL_NAME = "user_full_name" - val RESULT_USER_FIRST_NAME = "user_first_name" - val RESULT_USER_PROFILE_LARGEST_PIC_URL = "user_profile_largest_pic_url" - val RESULT_USER_PROFILE_SMALLEST_PIC_URL = "user_profile_smallest_pic_url" - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - supportFragmentManager.beginTransaction() - .replace(android.R.id.content, UserSelectorFragment.newInstance()) - .commit() - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportActionBar?.setTitle(R.string.prompt_select_guilty_person) - } - - override fun onUserSelected(user: User) { - val intent = Intent() - intent.putExtra(RESULT_USER_ID, user.id) - intent.putExtra(RESULT_USER_FULL_NAME, user.profile.real_name) - intent.putExtra(RESULT_USER_FIRST_NAME, user.profile.first_name) - intent.putExtra(RESULT_USER_PROFILE_LARGEST_PIC_URL, user.profile.largestAvailableImage) - intent.putExtra(RESULT_USER_PROFILE_SMALLEST_PIC_URL, user.profile.smallestAvailableImage) - - setResult(RESULT_OK, intent) - finish() - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - finish() - } - - return super.onOptionsItemSelected(item) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorAdapter.kt b/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorAdapter.kt index 4525bab..1e414ac 100644 --- a/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorAdapter.kt +++ b/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorAdapter.kt @@ -6,7 +6,7 @@ import android.view.View import android.view.ViewGroup import com.bumptech.glide.Glide import com.codemate.koffeemate.R -import com.codemate.koffeemate.data.network.models.User +import com.codemate.koffeemate.data.models.User import kotlinx.android.synthetic.main.recycler_item_user.view.* import org.jetbrains.anko.onClick import java.util.* @@ -37,7 +37,7 @@ class UserSelectorAdapter(val onUserSelectedListener: (user: User) -> Unit) : inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(user: User) = with(itemView) { Glide.with(context) - .load(user.profile.image_72) + .load(user.profile.smallestAvailableImage) .into(profileImage) userName.text = user.profile.real_name diff --git a/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorFragment.kt b/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorFragment.kt index d4b9da2..3354ea9 100644 --- a/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorFragment.kt +++ b/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorFragment.kt @@ -28,7 +28,7 @@ import android.view.ViewGroup import com.codemate.koffeemate.KoffeemateApp import com.codemate.koffeemate.R import com.codemate.koffeemate.common.BasicListItemAnimator -import com.codemate.koffeemate.data.network.models.User +import com.codemate.koffeemate.data.models.User import kotlinx.android.synthetic.main.fragment_user_selector.* import kotlinx.android.synthetic.main.fragment_user_selector.view.* import org.jetbrains.anko.onClick @@ -38,12 +38,22 @@ class UserSelectorFragment : DialogFragment(), UserSelectorView { private lateinit var userSelectorAdapter: UserSelectorAdapter private lateinit var userSelectListener: UserSelectListener + private var requestCode: Int = 0 + @Inject lateinit var presenter: UserSelectorPresenter companion object { - fun newInstance(): UserSelectorFragment { + private val ARG_TITLE = "title" + private val ARG_REQUEST_CODE = "request_code" + + fun newInstance(title: String, requestCode: Int): UserSelectorFragment { + val args = Bundle() + args.putString(ARG_TITLE, title) + args.putInt(ARG_REQUEST_CODE, requestCode) + val fragment = UserSelectorFragment() + fragment.arguments = args fragment.setStyle(DialogFragment.STYLE_NORMAL, R.style.TitledDialog) return fragment @@ -51,7 +61,7 @@ class UserSelectorFragment : DialogFragment(), UserSelectorView { } interface UserSelectListener { - fun onUserSelected(user: User) + fun onUserSelected(user: User, requestCode: Int) } @Suppress("UNCHECKED_CAST") @@ -72,8 +82,9 @@ class UserSelectorFragment : DialogFragment(), UserSelectorView { override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - KoffeemateApp.appComponent.inject(this) + + requestCode = arguments.getInt(ARG_REQUEST_CODE) setUpUserRecycler() presenter.attachView(this) @@ -86,14 +97,15 @@ class UserSelectorFragment : DialogFragment(), UserSelectorView { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = super.onCreateDialog(savedInstanceState) - dialog.setTitle(R.string.prompt_select_person_below) + val title = arguments.getString(ARG_TITLE) + dialog.setTitle(title) return dialog } private fun setUpUserRecycler() { userSelectorAdapter = UserSelectorAdapter { user -> - userSelectListener.onUserSelected(user) + userSelectListener.onUserSelected(user, requestCode) dismiss() } diff --git a/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorPresenter.kt b/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorPresenter.kt index c353dad..94106f7 100644 --- a/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorPresenter.kt +++ b/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorPresenter.kt @@ -1,7 +1,8 @@ package com.codemate.koffeemate.ui.userselector -import com.codemate.koffeemate.data.network.models.User +import com.codemate.koffeemate.data.models.User import com.codemate.koffeemate.ui.base.BasePresenter +import com.codemate.koffeemate.usecases.LoadUsersUseCase import rx.Subscriber import javax.inject.Inject diff --git a/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorView.kt b/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorView.kt index 0e4f05e..46b6858 100644 --- a/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorView.kt +++ b/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorView.kt @@ -1,6 +1,6 @@ package com.codemate.koffeemate.ui.userselector -import com.codemate.koffeemate.data.network.models.User +import com.codemate.koffeemate.data.models.User import com.codemate.koffeemate.ui.base.MvpView interface UserSelectorView : MvpView { diff --git a/app/src/main/java/com/codemate/koffeemate/ui/userselector/LoadUsersUseCase.kt b/app/src/main/java/com/codemate/koffeemate/usecases/LoadUsersUseCase.kt similarity index 51% rename from app/src/main/java/com/codemate/koffeemate/ui/userselector/LoadUsersUseCase.kt rename to app/src/main/java/com/codemate/koffeemate/usecases/LoadUsersUseCase.kt index 2c62d74..48fa8bc 100644 --- a/app/src/main/java/com/codemate/koffeemate/ui/userselector/LoadUsersUseCase.kt +++ b/app/src/main/java/com/codemate/koffeemate/usecases/LoadUsersUseCase.kt @@ -14,22 +14,39 @@ * limitations under the License. */ -package com.codemate.koffeemate.ui.userselector +package com.codemate.koffeemate.usecases import com.codemate.koffeemate.BuildConfig +import com.codemate.koffeemate.data.local.UserRepository +import com.codemate.koffeemate.data.models.User +import com.codemate.koffeemate.data.models.isFreshEnough import com.codemate.koffeemate.data.network.SlackApi -import com.codemate.koffeemate.data.network.models.User -import com.codemate.koffeemate.data.network.models.UserListResponse import rx.Observable import rx.Scheduler +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Named -open class LoadUsersUseCase( +open class LoadUsersUseCase @Inject constructor( + var userRepository: UserRepository, var slackApi: SlackApi, - var subscriber: Scheduler, - var observer: Scheduler + @Named("subscriber") var subscriber: Scheduler, + @Named("observer") var observer: Scheduler ) { + val MAX_CACHE_STALENESS = TimeUnit.HOURS.toMillis(12) + fun execute(): Observable> { - return slackApi.getUsers(BuildConfig.SLACK_AUTH_TOKEN) + val currentTime = System.currentTimeMillis() + val cachedUsers = Observable.just(userRepository.getAll()) + val networkUsers = slackApi.getUsers(BuildConfig.SLACK_AUTH_TOKEN) + .flatMap { userResponse -> + val usersWithTimestamp = userResponse.members.toMutableList() + usersWithTimestamp.forEach { + it.last_updated = currentTime + } + + Observable.just(usersWithTimestamp) + } .subscribeOn(subscriber) .observeOn(observer) .map { @@ -37,10 +54,17 @@ open class LoadUsersUseCase( it.profile.real_name } } + .doOnNext { userRepository.addAll(it) } + + return Observable + .concat(cachedUsers, networkUsers) + .first { + it.isNotEmpty() && it.isFreshEnough(MAX_CACHE_STALENESS) + } } - private fun filterNonCompanyUsers(response: UserListResponse): List { - return response.members.filter { + private fun filterNonCompanyUsers(response: List): List { + return response.filter { !it.is_bot // At Codemate, profiles starting with "Ext-" aren't employees, // but customers instead: they don't hang out in the office. diff --git a/app/src/main/java/com/codemate/koffeemate/ui/main/PostAccidentUseCase.kt b/app/src/main/java/com/codemate/koffeemate/usecases/PostAccidentUseCase.kt similarity index 82% rename from app/src/main/java/com/codemate/koffeemate/ui/main/PostAccidentUseCase.kt rename to app/src/main/java/com/codemate/koffeemate/usecases/PostAccidentUseCase.kt index 6bbc060..aa23397 100644 --- a/app/src/main/java/com/codemate/koffeemate/ui/main/PostAccidentUseCase.kt +++ b/app/src/main/java/com/codemate/koffeemate/usecases/PostAccidentUseCase.kt @@ -14,12 +14,13 @@ * limitations under the License. */ -package com.codemate.koffeemate.ui.main +package com.codemate.koffeemate.usecases import android.graphics.Bitmap import com.codemate.koffeemate.common.AwardBadgeCreator import com.codemate.koffeemate.data.local.CoffeeEventRepository import com.codemate.koffeemate.data.local.CoffeePreferences +import com.codemate.koffeemate.data.models.User import com.codemate.koffeemate.data.network.SlackApi import com.codemate.koffeemate.extensions.toRequestBody import okhttp3.MediaType @@ -29,28 +30,29 @@ import okhttp3.ResponseBody import retrofit2.Response import rx.Observable import rx.Scheduler +import javax.inject.Inject +import javax.inject.Named -open class PostAccidentUseCase( +open class PostAccidentUseCase @Inject constructor( var slackApi: SlackApi, val coffeeEventRepository: CoffeeEventRepository, val coffeePreferences: CoffeePreferences, val awardBadgeCreator: AwardBadgeCreator, - var subscriber: Scheduler, - var observer: Scheduler + @Named("subscriber") var subscriber: Scheduler, + @Named("observer") var observer: Scheduler ) { fun execute( comment: String, - userId: String, - userName: String, + user: User, profilePic: Bitmap ): Observable> { - coffeeEventRepository.recordBrewingAccident(userId) + coffeeEventRepository.recordBrewingAccident(user) - val awardCount = coffeeEventRepository.getAccidentCountForUser(userId) + val awardCount = coffeeEventRepository.getAccidentCountForUser(user) val profilePicWithAward = awardBadgeCreator.createBitmapFileWithAward(profilePic, awardCount) // Evaluates to "johns-certificate.png" etc - val fileName = "${userName.toLowerCase()}s-certificate.png" + val fileName = "${user.profile.first_name.toLowerCase()}s-certificate.png" val channel = coffeePreferences.getAccidentChannel() return slackApi.postImage( diff --git a/app/src/main/java/com/codemate/koffeemate/ui/main/SendCoffeeAnnouncementUseCase.kt b/app/src/main/java/com/codemate/koffeemate/usecases/SendCoffeeAnnouncementUseCase.kt similarity index 79% rename from app/src/main/java/com/codemate/koffeemate/ui/main/SendCoffeeAnnouncementUseCase.kt rename to app/src/main/java/com/codemate/koffeemate/usecases/SendCoffeeAnnouncementUseCase.kt index ad04100..6e94b48 100644 --- a/app/src/main/java/com/codemate/koffeemate/ui/main/SendCoffeeAnnouncementUseCase.kt +++ b/app/src/main/java/com/codemate/koffeemate/usecases/SendCoffeeAnnouncementUseCase.kt @@ -14,18 +14,20 @@ * limitations under the License. */ -package com.codemate.koffeemate.ui.main +package com.codemate.koffeemate.usecases import com.codemate.koffeemate.data.network.SlackApi import okhttp3.ResponseBody import retrofit2.Response import rx.Observable import rx.Scheduler +import javax.inject.Inject +import javax.inject.Named -open class SendCoffeeAnnouncementUseCase( +open class SendCoffeeAnnouncementUseCase @Inject constructor( var slackApi: SlackApi, - var subscriber: Scheduler, - var observer: Scheduler + @Named("subscriber") var subscriber: Scheduler, + @Named("observer") var observer: Scheduler ) { fun execute(channel: String, newCoffeeMessage: String): Observable> { return slackApi.postMessage(channel, newCoffeeMessage) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 460093f..12462d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -63,7 +63,7 @@ Reset the counter? - Who is guilty of this coffee brewing accident? + Who failed coffee brewing? Who is brewing? Select a guilty person diff --git a/app/src/test/java/com/codemate/koffeemate/testutils/CommonTestUtils.kt b/app/src/test/java/com/codemate/koffeemate/testutils/CommonTestUtils.kt index 3cda3eb..3cccba6 100644 --- a/app/src/test/java/com/codemate/koffeemate/testutils/CommonTestUtils.kt +++ b/app/src/test/java/com/codemate/koffeemate/testutils/CommonTestUtils.kt @@ -16,8 +16,8 @@ package com.codemate.koffeemate.testutils -import com.codemate.koffeemate.data.network.models.Profile -import com.codemate.koffeemate.data.network.models.User +import com.codemate.koffeemate.data.models.Profile +import com.codemate.koffeemate.data.models.User import java.io.File fun Any.getResourceFile(path: String): File { diff --git a/app/src/test/java/com/codemate/koffeemate/ui/main/MainPresenterTest.kt b/app/src/test/java/com/codemate/koffeemate/ui/main/MainPresenterTest.kt index 1abe82f..3682c44 100644 --- a/app/src/test/java/com/codemate/koffeemate/ui/main/MainPresenterTest.kt +++ b/app/src/test/java/com/codemate/koffeemate/ui/main/MainPresenterTest.kt @@ -8,10 +8,12 @@ import com.codemate.koffeemate.common.BrewingProgressUpdater import com.codemate.koffeemate.common.ScreenSaver import com.codemate.koffeemate.data.local.CoffeeEventRepository import com.codemate.koffeemate.data.local.CoffeePreferences -import com.codemate.koffeemate.data.local.models.CoffeeBrewingEvent +import com.codemate.koffeemate.data.models.CoffeeBrewingEvent import com.codemate.koffeemate.data.network.SlackApi import com.codemate.koffeemate.testutils.fakeUser import com.codemate.koffeemate.testutils.getResourceFile +import com.codemate.koffeemate.usecases.PostAccidentUseCase +import com.codemate.koffeemate.usecases.SendCoffeeAnnouncementUseCase import com.nhaarman.mockito_kotlin.* import okhttp3.MediaType import okhttp3.ResponseBody @@ -131,7 +133,7 @@ class MainPresenterTest { verify(view).updateCoffeeProgress(0) verify(view).resetCoffeeViewStatus() - verify(mockCoffeeEventRepository).recordBrewingEvent(user.id) + verify(mockCoffeeEventRepository).recordBrewingEvent(user) } @Test @@ -226,12 +228,7 @@ class MainPresenterTest { presenter.personBrewingCoffee = user presenter.launchAccidentReportingScreen() - verify(view).showPostAccidentAnnouncementPrompt( - user.id, - user.profile.real_name, - user.profile.first_name, - user.profile.largestAvailableImage - ) + verify(view).showPostAccidentAnnouncementPrompt(user) verifyNoMoreInteractions(view) } @@ -273,7 +270,7 @@ class MainPresenterTest { .thenReturn(emptySuccessResponse) presenter.personBrewingCoffee = fakeUser() - presenter.announceCoffeeBrewingAccident("", "", "", mock()) + presenter.announceCoffeeBrewingAccident("", fakeUser(), mock()) assertThat(presenter.personBrewingCoffee, nullValue()) } @@ -283,7 +280,7 @@ class MainPresenterTest { whenever(mockSlackApi.postImage(any(), any(), any(), any(), any())) .thenReturn(emptySuccessResponse) - presenter.announceCoffeeBrewingAccident("", "", "", mock()) + presenter.announceCoffeeBrewingAccident("", fakeUser(), mock()) verify(view).showAccidentPostedSuccessfullyMessage() verifyNoMoreInteractions(view) @@ -294,7 +291,7 @@ class MainPresenterTest { whenever(mockSlackApi.postImage(any(), any(), any(), any(), any())) .thenReturn(Observable.error(Throwable())) - presenter.announceCoffeeBrewingAccident("", "", "", mock()) + presenter.announceCoffeeBrewingAccident("", fakeUser(), mock()) verify(view).showErrorPostingAccidentMessage() } diff --git a/app/src/test/java/com/codemate/koffeemate/ui/userselector/UserSelectorPresenterTest.kt b/app/src/test/java/com/codemate/koffeemate/ui/userselector/UserSelectorPresenterTest.kt index 59f1bb0..3e9d3c7 100644 --- a/app/src/test/java/com/codemate/koffeemate/ui/userselector/UserSelectorPresenterTest.kt +++ b/app/src/test/java/com/codemate/koffeemate/ui/userselector/UserSelectorPresenterTest.kt @@ -2,23 +2,18 @@ package com.codemate.koffeemate.ui.userselector import android.graphics.Bitmap import com.codemate.koffeemate.common.AwardBadgeCreator -import com.codemate.koffeemate.data.local.CoffeeEventRepository import com.codemate.koffeemate.data.local.CoffeePreferences +import com.codemate.koffeemate.data.local.UserRepository +import com.codemate.koffeemate.data.models.UserListResponse import com.codemate.koffeemate.data.network.SlackApi -import com.codemate.koffeemate.data.network.models.Profile -import com.codemate.koffeemate.data.network.models.User -import com.codemate.koffeemate.data.network.models.UserListResponse import com.codemate.koffeemate.testutils.fakeUser import com.codemate.koffeemate.testutils.getResourceFile -import com.codemate.koffeemate.ui.main.PostAccidentUseCase +import com.codemate.koffeemate.usecases.LoadUsersUseCase import com.nhaarman.mockito_kotlin.* -import okhttp3.MediaType -import okhttp3.ResponseBody import org.junit.Before import org.junit.Test import org.mockito.Mock import org.mockito.MockitoAnnotations -import retrofit2.Response import rx.Observable import rx.schedulers.Schedulers @@ -51,6 +46,7 @@ class UserSelectorPresenterTest { MockitoAnnotations.initMocks(this) val loadUsersUseCase = LoadUsersUseCase( + mock(), mockSlackApi, Schedulers.immediate(), Schedulers.immediate() diff --git a/app/src/test/java/com/codemate/koffeemate/ui/userselector/LoadUsersUseCaseTest.kt b/app/src/test/java/com/codemate/koffeemate/usecases/LoadUsersUseCaseTest.kt similarity index 56% rename from app/src/test/java/com/codemate/koffeemate/ui/userselector/LoadUsersUseCaseTest.kt rename to app/src/test/java/com/codemate/koffeemate/usecases/LoadUsersUseCaseTest.kt index db7af43..9ad462e 100644 --- a/app/src/test/java/com/codemate/koffeemate/ui/userselector/LoadUsersUseCaseTest.kt +++ b/app/src/test/java/com/codemate/koffeemate/usecases/LoadUsersUseCaseTest.kt @@ -14,13 +14,15 @@ * limitations under the License. */ -package com.codemate.koffeemate.ui.userselector +package com.codemate.koffeemate.usecases import com.codemate.koffeemate.BuildConfig +import com.codemate.koffeemate.data.local.UserRepository +import com.codemate.koffeemate.data.models.User import com.codemate.koffeemate.data.network.SlackApi -import com.codemate.koffeemate.data.network.SlackService -import com.codemate.koffeemate.data.network.models.User import com.codemate.koffeemate.testutils.getResourceFile +import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockito_kotlin.whenever import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.hamcrest.core.IsEqual.equalTo @@ -28,10 +30,15 @@ import org.junit.After import org.junit.Assert.assertThat import org.junit.Before import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations import rx.observers.TestSubscriber import rx.schedulers.Schedulers class LoadUsersUseCaseTest { + @Mock + lateinit var mockUserRepository: UserRepository + lateinit var mockServer: MockWebServer lateinit var slackApi: SlackApi lateinit var useCase: LoadUsersUseCase @@ -39,11 +46,14 @@ class LoadUsersUseCaseTest { @Before fun setUp() { + MockitoAnnotations.initMocks(this) + mockServer = MockWebServer() mockServer.start() - slackApi = SlackService.getApi(mockServer.url("/")) + slackApi = SlackApi.create(mockServer.url("/")) useCase = LoadUsersUseCase( + mockUserRepository, slackApi, Schedulers.immediate(), Schedulers.immediate() @@ -67,14 +77,73 @@ class LoadUsersUseCaseTest { } @Test - fun execute_WithValidData_CallsOnNextWithUsersAndCompletes() { - val userListJson = getResourceFile("seeds/sample_userlist_response.json").readText() - mockServer.enqueue(MockResponse().setBody(userListJson)) + fun execute_WhenLoadingUsersFromApi_CachesThem() { + enqueUserJsonResponseFromServer() + + useCase.execute().subscribe(testSubscriber) + testSubscriber.assertValueCount(1) + + val userList = testSubscriber.onNextEvents[0] + verify(mockUserRepository).addAll(userList) + } + + @Test + fun execute_WhenHasNoCachedUsers_LoadsThemFromApi() { + enqueUserJsonResponseFromServer() + + useCase.execute().subscribe(testSubscriber) + testSubscriber.assertValueCount(1) + + val userList = testSubscriber.onNextEvents[0] + verifyUsersFromApi(userList) + } + + @Test + fun execute_WhenHasFreshEnoughCachedUsers_LoadsThemFromCache() { + val currentTime = System.currentTimeMillis() + val cachedUsers = listOf( + User(last_updated = currentTime), + User(last_updated = currentTime), + User(last_updated = currentTime) + ) + + whenever(mockUserRepository.getAll()).thenReturn(cachedUsers) + enqueUserJsonResponseFromServer() useCase.execute().subscribe(testSubscriber) testSubscriber.assertValueCount(1) val userList = testSubscriber.onNextEvents[0] + assertThat(userList.size, equalTo(3)) + assertThat(userList, equalTo(cachedUsers)) + } + + @Test + fun execute_WhenHasTooOldCachedUsers_LoadsThemFromApi() { + val currentTime = System.currentTimeMillis() - useCase.MAX_CACHE_STALENESS + val cachedUsers = listOf( + User(last_updated = currentTime), + User(last_updated = currentTime), + User(last_updated = currentTime) + ) + + whenever(mockUserRepository.getAll()).thenReturn(cachedUsers) + enqueUserJsonResponseFromServer() + + useCase.execute().subscribe(testSubscriber) + testSubscriber.assertValueCount(1) + + val userList = testSubscriber.onNextEvents[0] + verifyUsersFromApi(userList) + } + + // Utility functions --> + private fun enqueUserJsonResponseFromServer() { + val userListJson = getResourceFile("seeds/sample_userlist_response.json").readText() + mockServer.enqueue(MockResponse().setBody(userListJson)) + } + + private fun verifyUsersFromApi(userList: List) { assertThat(userList.size, equalTo(2)) with(userList[0]) { diff --git a/app/src/test/java/com/codemate/koffeemate/ui/userselector/PostAccidentUseCaseTest.kt b/app/src/test/java/com/codemate/koffeemate/usecases/PostAccidentUseCaseTest.kt similarity index 75% rename from app/src/test/java/com/codemate/koffeemate/ui/userselector/PostAccidentUseCaseTest.kt rename to app/src/test/java/com/codemate/koffeemate/usecases/PostAccidentUseCaseTest.kt index e6f883a..3e35ed5 100644 --- a/app/src/test/java/com/codemate/koffeemate/ui/userselector/PostAccidentUseCaseTest.kt +++ b/app/src/test/java/com/codemate/koffeemate/usecases/PostAccidentUseCaseTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.codemate.koffeemate.ui.userselector +package com.codemate.koffeemate.usecases import android.content.SharedPreferences import android.graphics.Bitmap @@ -23,18 +23,10 @@ import com.codemate.koffeemate.common.AwardBadgeCreator import com.codemate.koffeemate.data.local.CoffeeEventRepository import com.codemate.koffeemate.data.local.CoffeePreferences import com.codemate.koffeemate.data.network.SlackApi -import com.codemate.koffeemate.data.network.SlackService -import com.codemate.koffeemate.data.network.models.Profile -import com.codemate.koffeemate.data.network.models.User -import com.codemate.koffeemate.testutils.RegexMatcher +import com.codemate.koffeemate.testutils.RegexMatcher.Companion.matchesPattern import com.codemate.koffeemate.testutils.fakeUser import com.codemate.koffeemate.testutils.getResourceFile -import com.codemate.koffeemate.ui.main.PostAccidentUseCase -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.verify -import com.nhaarman.mockito_kotlin.verifyNoMoreInteractions -import com.nhaarman.mockito_kotlin.whenever -import okhttp3.Dispatcher +import com.nhaarman.mockito_kotlin.* import okhttp3.ResponseBody import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer @@ -50,8 +42,6 @@ import rx.observers.TestSubscriber import rx.schedulers.Schedulers class PostAccidentUseCaseTest { - val TEST_USER_ID = "abc123" - @Mock lateinit var mockCoffeePreferences: CoffeePreferences @@ -79,12 +69,12 @@ class PostAccidentUseCaseTest { mockCoffeePreferences.preferences = mock() whenever(mockCoffeePreferences.getAccidentChannel()).thenReturn("test-channel") - whenever(mockCoffeeEventRepository.getAccidentCountForUser(TEST_USER_ID)).thenReturn(1) + whenever(mockCoffeeEventRepository.getAccidentCountForUser(any())).thenReturn(1) whenever(mockAwardBadgeCreator.createBitmapFileWithAward(mockBitmap, 1)) .thenReturn(getResourceFile("images/empty.png")) - slackApi = SlackService.getApi(mockServer.url("/")) + slackApi = SlackApi.create(mockServer.url("/")) useCase = PostAccidentUseCase( slackApi, mockCoffeeEventRepository, @@ -105,26 +95,26 @@ class PostAccidentUseCaseTest { @Test fun announceCoffeeBrewingAccident_ShouldMakeCorrectRequest() { val user = fakeUser() - useCase.execute("Test comment", user.id, user.profile.first_name, mockBitmap).subscribe(testSubscriber) + useCase.execute("Test comment", user, mockBitmap).subscribe(testSubscriber) // TODO: There has to be a better way to verify these multipart post params, right? :S val requestBody = mockServer.takeRequest().body.readUtf8() assertThat(requestBody, StringContains.containsString("filename=\"jormas-certificate.png\"")) - assertThat(requestBody, RegexMatcher.matchesPattern(".*channels.*test-channel.*")) - assertThat(requestBody, RegexMatcher.matchesPattern(".*initial_comment.*Test comment.*")) - assertThat(requestBody, RegexMatcher.matchesPattern(".*token.*${BuildConfig.SLACK_AUTH_TOKEN}.*")) + assertThat(requestBody, matchesPattern(".*channels.*test-channel.*")) + assertThat(requestBody, matchesPattern(".*initial_comment.*Test comment.*")) + assertThat(requestBody, matchesPattern(".*token.*${BuildConfig.SLACK_AUTH_TOKEN}.*")) } @Test fun announceCoffeeBrewingAccident_WhenSuccessful_NotifiesUIAndStoresEvent() { val user = fakeUser() - useCase.execute("", user.id, user.profile.first_name, mockBitmap).subscribe(testSubscriber) + useCase.execute("", user, mockBitmap).subscribe(testSubscriber) testSubscriber.assertValueCount(1) testSubscriber.assertCompleted() - verify(mockCoffeeEventRepository).recordBrewingAccident(user.id) - verify(mockCoffeeEventRepository).getAccidentCountForUser(TEST_USER_ID) + verify(mockCoffeeEventRepository).recordBrewingAccident(user) + verify(mockCoffeeEventRepository).getAccidentCountForUser(user) verifyNoMoreInteractions(mockCoffeeEventRepository) } } \ No newline at end of file diff --git a/app/src/test/java/com/codemate/koffeemate/ui/main/SendCoffeeAnnouncementUseCaseTest.kt b/app/src/test/java/com/codemate/koffeemate/usecases/SendCoffeeAnnouncementUseCaseTest.kt similarity index 93% rename from app/src/test/java/com/codemate/koffeemate/ui/main/SendCoffeeAnnouncementUseCaseTest.kt rename to app/src/test/java/com/codemate/koffeemate/usecases/SendCoffeeAnnouncementUseCaseTest.kt index a7889a7..915b689 100644 --- a/app/src/test/java/com/codemate/koffeemate/ui/main/SendCoffeeAnnouncementUseCaseTest.kt +++ b/app/src/test/java/com/codemate/koffeemate/usecases/SendCoffeeAnnouncementUseCaseTest.kt @@ -14,12 +14,10 @@ * limitations under the License. */ -package com.codemate.koffeemate.ui.main +package com.codemate.koffeemate.usecases import com.codemate.koffeemate.BuildConfig import com.codemate.koffeemate.data.network.SlackApi -import com.codemate.koffeemate.data.network.SlackService -import okhttp3.Dispatcher import okhttp3.ResponseBody import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer @@ -49,7 +47,7 @@ class SendCoffeeAnnouncementUseCaseTest { mockServer = MockWebServer() mockServer.start() - slackApi = SlackService.getApi(mockServer.url("/")) + slackApi = SlackApi.create(mockServer.url("/")) useCase = SendCoffeeAnnouncementUseCase( slackApi, Schedulers.immediate(),