Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import com.nativeapptemplate.nativeapptemplatefree.data.login.AppLanguageSyncer
import com.nativeapptemplate.nativeapptemplatefree.model.Attributes
import com.nativeapptemplate.nativeapptemplatefree.model.Data
import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper
import com.nativeapptemplate.nativeapptemplatefree.model.Meta
import com.nativeapptemplate.nativeapptemplatefree.model.Permissions
import com.nativeapptemplate.nativeapptemplatefree.model.UserData
import com.nativeapptemplate.nativeapptemplatefree.testing.repository.TestAppLanguageProvider
import com.nativeapptemplate.nativeapptemplatefree.testing.repository.TestLoginRepository
Expand Down Expand Up @@ -84,6 +86,37 @@ class MainActivityViewModelTest {
assertTrue(loginRepository.userData.first().didShowTapShopBelowTip)
}

@Test
fun updatePermissions_whenSignedIn_savesThePermissions() = runTest {
loginRepository.sendUserData(UserData())
loginRepository.sendIsLoggedIn(true)
loginRepository.sendPermissions(
Permissions(meta = Meta(androidAppVersion = 7, shouldUpdatePrivacy = true, shouldUpdateTerms = false, shopLimitCount = 10)),
)

viewModel.updatePermissions()

val userData = loginRepository.userData.first()
assertEquals(7, userData.androidAppVersion)
assertTrue(userData.shouldUpdatePrivacy)
assertEquals(10, userData.shopLimitCount)
}

@Test
fun updatePermissions_whenSignedOut_fetchesNothing() = runTest {
loginRepository.sendUserData(UserData())
loginRepository.sendIsLoggedIn(false)
loginRepository.sendPermissions(
Permissions(meta = Meta(androidAppVersion = 7, shouldUpdatePrivacy = true, shouldUpdateTerms = false, shopLimitCount = 10)),
)

viewModel.updatePermissions()

// UserData's defaults: nothing was saved.
assertEquals(-1, loginRepository.userData.first().androidAppVersion)
assertEquals(0, loginRepository.logoutCallCount)
}

@Test
fun updatePermissions_whenOffline_keepsTheUserSignedIn() = runTest {
loginRepository.sendIsLoggedIn(true)
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.nativeapptemplate.nativeapptemplatefree.datastore

import com.nativeapptemplate.nativeapptemplatefree.BuildConfig
import com.nativeapptemplate.nativeapptemplatefree.UserPreferences
import com.nativeapptemplate.nativeapptemplatefree.datastoreTest.InMemoryDataStore
import com.nativeapptemplate.nativeapptemplatefree.di.modules.networkJson
import com.nativeapptemplate.nativeapptemplatefree.model.Attributes
import com.nativeapptemplate.nativeapptemplatefree.model.DarkThemeConfig
import com.nativeapptemplate.nativeapptemplatefree.model.Data
import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper
import com.nativeapptemplate.nativeapptemplatefree.model.Meta
Expand Down Expand Up @@ -154,6 +156,90 @@ class NativeAppTemplatePreferencesDataSourceTest {
assertTrue(userData.isLoggedIn)
assertEquals("", userData.accountName)
}

@Test
fun setShopkeeper_storesTheSessionFromTheSignInResponse() = testScope.runTest {
subject.setShopkeeper(testInputLoggedInShopkeeper)

val userData = subject.userData.first()
assertEquals(LOGGED_IN_SHOPKEEPER_ID, userData.id)
assertEquals(LOGGED_IN_SHOPKEEPER_ACCOUNT_ID, userData.accountId)
assertEquals(LOGGED_IN_SHOPKEEPER_TOKEN, userData.token)
assertEquals(LOGGED_IN_SHOPKEEPER_CLIENT, userData.client)
assertEquals(LOGGED_IN_SHOPKEEPER_UID, userData.uid)
assertEquals(LOGGED_IN_SHOPKEEPER_EXPIRY, userData.expiry)
assertEquals(LOGGED_IN_SHOPKEEPER_TIME_ZONE, userData.timeZone)
}

// shouldUpdateApp = this build's versionCode (BuildConfig.VERSION_CODE) is older than the
// android_app_version the server requires.
@Test
fun setPermissions_aNewerRequiredAppVersion_requiresAnAppUpdate() = testScope.runTest {
subject.setPermissions(Permissions(meta = Meta(androidAppVersion = BuildConfig.VERSION_CODE + 1)))

assertTrue(subject.shouldUpdateApp().first())
assertEquals(BuildConfig.VERSION_CODE + 1, subject.userData.first().androidAppVersion)
}

@Test
fun setPermissions_theCurrentAppVersion_requiresNoAppUpdate() = testScope.runTest {
subject.setPermissions(Permissions(meta = Meta(androidAppVersion = BuildConfig.VERSION_CODE + 1)))

// The required version is this build's own: no longer older.
subject.setPermissions(Permissions(meta = Meta(androidAppVersion = BuildConfig.VERSION_CODE)))

assertFalse(subject.shouldUpdateApp().first())
}

@Test
fun darkThemeConfig_isFollowSystemUntilSet_thenWhatWasSet() = testScope.runTest {
// DARK_THEME_CONFIG_UNSPECIFIED (proto3's default 0) reads as FOLLOW_SYSTEM.
assertEquals(DarkThemeConfig.FOLLOW_SYSTEM, subject.userData.first().darkThemeConfig)

DarkThemeConfig.entries.forEach { config ->
subject.setDarkThemeConfig(config)
assertEquals(config, subject.userData.first().darkThemeConfig)
}
}

// Signing out (LoginRepositoryImpl.logout) clears the whole UserPreferences: the session, and also
// the device-level settings stored next to it (dark mode, the shop tip). Current behavior.
@Test
fun clearUserPreferences_resetsEverythingToTheDefaults() = testScope.runTest {
subject.setShopkeeper(testInputLoggedInShopkeeper)
subject.setPermissions(Permissions(meta = Meta(androidAppVersion = BuildConfig.VERSION_CODE + 1, shouldUpdatePrivacy = true, shouldUpdateTerms = true, shopLimitCount = 99)))
subject.setDarkThemeConfig(DarkThemeConfig.DARK)
subject.setDidShowTapShopBelowTip(true)
subject.setIsShopDeleted(true)

subject.clearUserPreferences()

val userData = subject.userData.first()
assertFalse(userData.isLoggedIn)
assertEquals("", userData.token)
assertEquals("", userData.accountId)
assertFalse(subject.shouldUpdateApp().first())
assertFalse(subject.shouldUpdatePrivacy().first())
assertEquals(DarkThemeConfig.FOLLOW_SYSTEM, userData.darkThemeConfig)
assertFalse(subject.didShowTapShopBelowTip().first())
assertFalse(subject.isShopDeleted().first())
}

@Test
fun flags_readBackWhatWasSet() = testScope.runTest {
subject.setIsEmailUpdated(true)
subject.setIsMyAccountDeleted(true)
subject.setIsShopDeleted(true)
subject.setDidShowTapShopBelowTip(true)

assertTrue(subject.isEmailUpdated().first())
assertTrue(subject.isMyAccountDeleted().first())
assertTrue(subject.isShopDeleted().first())
assertTrue(subject.didShowTapShopBelowTip().first())

subject.setIsShopDeleted(false)
assertFalse(subject.isShopDeleted().first())
}
}

private const val LOGGED_IN_SHOPKEEPER_TYPE = "shopkeeper_sign_in"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,59 @@ class ApiResponseExtensionsTest {

assertEquals("HTTP 502", exception.rawMessage)
}

@Test
fun emitApiResponse_deviseFullMessagesBody_showsTheFirstMessage() = runTest {
// devise_token_auth's registration errors: {"errors": {"<field>": [...], "full_messages": [...]}}.
val response = httpError(
422,
"""{"status":"error","errors":{"email":["has already been taken"],"full_messages":["Email has already been taken","Password is too short"]}}""",
)

val exception = assertFailsWith<ApiException.UnprocessableError> {
flow { emitApiResponse(response) }.first()
}

assertEquals("Email has already been taken", exception.rawMessage)
assertEquals(422, exception.httpStatusCode)
}

@Test
fun emitApiResponse_jsonBodyWithoutErrors_showsTheHttpStatus() = runTest {
// Valid JSON, but neither {code, error_message} nor devise's "errors".
val response = httpError(500, """{"status":500,"error":"Internal Server Error"}""")

val exception = assertFailsWith<ApiException.UnprocessableError> {
flow { emitApiResponse(response) }.first()
}

assertEquals("HTTP 500", exception.rawMessage)
}

@Test
fun emitApiResponse_emptyErrorBody_showsTheHttpStatus() = runTest {
val response = httpError(503, "")

val exception = assertFailsWith<ApiException.UnprocessableError> {
flow { emitApiResponse(response) }.first()
}

assertEquals("HTTP 503", exception.rawMessage)
assertEquals(503, exception.httpStatusCode)
}

@Test
fun throwApiError_apiErrorBody_takesTheCodeFromTheBodyAndTheStatusFromTheResponse() {
// The two can differ; ApiError.code (shown as "[Status: code]") is the body's.
val response = httpError(403, """{"code":1001,"error_message":"Shop limit reached"}""")

val exception = assertFailsWith<ApiException.ApiError> {
throwApiError(response, "forbidden")
}

assertEquals(1001, exception.code)
assertEquals(403, exception.httpStatusCode)
// ApiException.ApiError: "[NATIVEAPPTEMPLATE-2001] " + "$apiMessage [Status: $code]".
assertEquals("[NATIVEAPPTEMPLATE-2001] Shop limit reached [Status: 1001]", exception.formattedDescription)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,20 @@ class AuthInterceptorTest {
assertNull(sent.header("access-token"))
}

@Test
fun intercept_apiHostConfiguredInAnotherCase_isStillTheApiHost() = testScope.runTest {
// Host names are case-insensitive (RFC 4343), and OkHttp lowercases the request's host, so a
// NATIVEAPPTEMPLATE_API_DOMAIN written with capitals must still match.
val interceptor = AuthInterceptor(
dataSourceWith(token = "test-token", client = "test-client", uid = "john@example.com", expiry = "12345"),
apiHost = "Example.COM",
)

val sent = interceptor.sentRequest(Request.Builder().url("https://example.com/").build())

assertEquals("test-token", sent.header("access-token"))
}

@Test
fun intercept_preservesOriginalRequestUrl() = testScope.runTest {
val dataSource = NativeAppTemplatePreferencesDataSource(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ class TestLoginRepository : LoginRepository {

override val userData: Flow<UserData> = _userData.filterNotNull()

override fun login(login: Login): Flow<LoggedInShopkeeper> = loggedInShopkeeperFlow
private var loginError: Throwable? = null

override fun login(login: Login): Flow<LoggedInShopkeeper> = loginError?.let { error -> flow { throw error } } ?: loggedInShopkeeperFlow

/** How many times [logout] was collected. */
var logoutCallCount = 0
Expand Down Expand Up @@ -176,6 +178,11 @@ class TestLoginRepository : LoginRepository {
permissionsFlow.tryEmit(permissions)
}

/** Makes every later [login] call fail with [error]. */
fun failLogin(error: Throwable) {
loginError = error
}

/** Makes every later [getPermissions] call fail with [error]. */
fun failPermissions(error: Throwable) {
permissionsError = error
Expand Down
Loading
Loading