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
@@ -1,7 +1,8 @@
package org.commcare.connect.network.connectId.parser

import org.commcare.android.database.connect.models.ConnectUserRecord
import org.commcare.connect.ConnectConstants
import org.commcare.connect.ConnectConstants.CONNECT_KEY_EXPIRES
import org.commcare.connect.ConnectConstants.CONNECT_KEY_TOKEN
import org.commcare.connect.network.base.BaseApiResponseParser
import org.commcare.core.network.AuthInfo.TokenAuth
import org.javarosa.core.io.StreamsUtil
Expand All @@ -10,29 +11,33 @@ import org.json.JSONObject
import java.io.InputStream
import java.util.Date

class ConnectTokenResponseParser<T>() : BaseApiResponseParser<T> {
override fun parse(responseCode: Int, responseData: InputStream, anyInputObject: Any?): T {
class ConnectTokenResponseParser<T> : BaseApiResponseParser<T> {
override fun parse(
responseCode: Int,
responseData: InputStream,
anyInputObject: Any?,
): T {
try {
responseData.use {
val responseAsString = String(
StreamsUtil.inputStreamToByteArray(
responseData
val responseAsString =
String(
StreamsUtil.inputStreamToByteArray(
responseData,
),
)
)

val json = JSONObject(responseAsString)
var key = ConnectConstants.CONNECT_KEY_TOKEN
val token = json.getString(key)
val token = json.getString(CONNECT_KEY_TOKEN)
check(!token.isNullOrEmpty() && token != "null") { "$CONNECT_KEY_TOKEN cannot be null or empty" }
val seconds = json.optInt(CONNECT_KEY_EXPIRES, 0)
check(seconds >= 0) { "$CONNECT_KEY_EXPIRES cannot be negative" }
val expiration = Date()
key = ConnectConstants.CONNECT_KEY_EXPIRES
val seconds = if (json.has(key)) json.getInt(key) else 0
expiration.time = expiration.time + (seconds.toLong() * 1000)
expiration.time += seconds.toLong() * 1000
(anyInputObject as ConnectUserRecord).updateConnectToken(token, expiration)
return TokenAuth(token) as T
}

} catch (e: JSONException) {
throw RuntimeException(e)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,28 @@ class WorkHistoryEarnedFragment : Fragment() {
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentWorkHistoryEarnedBinding.inflate(inflater, container, false)
return binding!!.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
workHistoryEarnedAdapter = WorkHistoryEarnedAdapter(
listener = object : WorkHistoryEarnedAdapter.OnWorkHistoryItemClickListener {
override fun onWorkHistoryItemClick(workHistory: PersonalIdWorkHistory) {

}
},
profilePic = profilePic ?: ""
)
workHistoryEarnedAdapter =
WorkHistoryEarnedAdapter(
listener =
object : WorkHistoryEarnedAdapter.OnWorkHistoryItemClickListener {
override fun onWorkHistoryItemClick(workHistory: PersonalIdWorkHistory) {
}
},
profilePic = profilePic ?: "",
)
binding!!.rvEarnedWorkHistory.adapter = workHistoryEarnedAdapter
viewModel = ViewModelProvider(requireActivity())[PersonalIdWorkHistoryViewModel::class.java]
viewModel.earnedWorkHistory.observe(viewLifecycleOwner) { earnedList ->
Expand All @@ -60,7 +65,10 @@ class WorkHistoryEarnedFragment : Fragment() {
private const val ARG_USERNAME = "username"
private const val ARG_PROFILE_PIC = "profile_pic"

fun newInstance(username: String, profilePic: String): WorkHistoryEarnedFragment {
fun newInstance(
username: String,
profilePic: String,
): WorkHistoryEarnedFragment {
val fragment = WorkHistoryEarnedFragment()
val args = Bundle()
args.putString(ARG_USERNAME, username)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import org.commcare.android.database.connect.models.PersonalIdWorkHistory
import org.commcare.dalvik.databinding.FragmentWorkHistoryPendingBinding

class WorkHistoryPendingFragment : Fragment() {

private var binding: FragmentWorkHistoryPendingBinding? = null
private lateinit var workHistoryPendingAdapter: WorkHistoryPendingAdapter
private lateinit var viewModel: PersonalIdWorkHistoryViewModel
Expand All @@ -27,21 +26,28 @@ class WorkHistoryPendingFragment : Fragment() {
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentWorkHistoryPendingBinding.inflate(inflater, container, false)
return binding!!.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
workHistoryPendingAdapter = WorkHistoryPendingAdapter(listener = object :
WorkHistoryPendingAdapter.OnWorkHistoryItemClickListener {
override fun onWorkHistoryItemClick(workHistory: PersonalIdWorkHistory) {

}
})
workHistoryPendingAdapter =
WorkHistoryPendingAdapter(
listener =
object :
WorkHistoryPendingAdapter.OnWorkHistoryItemClickListener {
override fun onWorkHistoryItemClick(workHistory: PersonalIdWorkHistory) {
}
},
)
binding!!.rvPendingWorkHistory.adapter = workHistoryPendingAdapter
viewModel = ViewModelProvider(requireActivity())[PersonalIdWorkHistoryViewModel::class.java]
viewModel.pendingWorkHistory.observe(viewLifecycleOwner) { pendingList ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package org.commcare.connect.network.connectId.parser

import androidx.test.ext.junit.runners.AndroidJUnit4
import org.commcare.CommCareTestApplication
import org.commcare.android.database.connect.models.ConnectUserRecord
import org.commcare.connect.ConnectConstants
import org.commcare.core.network.AuthInfo
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import java.io.ByteArrayInputStream
import java.util.Date
import kotlin.math.abs

@Config(application = CommCareTestApplication::class)
@RunWith(AndroidJUnit4::class)
class ConnectTokenResponseParserTest {
private data class TokenTestData(
val token: String,
val expiresInSeconds: Int? = null,
val expectedTimeDeltaSeconds: Long? = null,
)

private fun createTokenJson(
token: String,
expiresValue: Any? = null,
): String =
if (expiresValue != null) {
"""
{
"${ConnectConstants.CONNECT_KEY_TOKEN}": "$token",
"${ConnectConstants.CONNECT_KEY_EXPIRES}": $expiresValue
}
""".trimIndent()
} else {
"""
{
"${ConnectConstants.CONNECT_KEY_TOKEN}": "$token"
}
""".trimIndent()
}

private fun createTokenJson(testData: TokenTestData): String = createTokenJson(testData.token, testData.expiresInSeconds)

private fun parseTokenAndAssertBasics(
jsonResponse: String,
expectedToken: String,
userRecord: ConnectUserRecord = ConnectUserRecord(),
): Pair<AuthInfo.TokenAuth, ConnectUserRecord> {
val parser = ConnectTokenResponseParser<AuthInfo.TokenAuth>()
val inputStream = ByteArrayInputStream(jsonResponse.toByteArray())
val result = parser.parse(200, inputStream, userRecord)

assertEquals(expectedToken, result.bearerToken)
assertEquals(expectedToken, userRecord.connectToken)

return Pair(result, userRecord)
}

private fun assertExpirationTime(
userRecord: ConnectUserRecord,
currentTime: Date,
expectedDeltaSeconds: Long,
tolerance: Long = 5000L,
) {
val expectedExpirationTime = currentTime.time + (expectedDeltaSeconds * 1000L)
val actualExpirationTime = userRecord.connectTokenExpiration.time
val timeDifference = abs(actualExpirationTime - expectedExpirationTime)
assertTrue("Expiration time should be within ${tolerance}ms of expected", timeDifference < tolerance)
}

private fun testTokenWithExpiration(testData: TokenTestData) {
// Arrange
val currentTime = Date()
val jsonResponse = createTokenJson(testData)

// Act & Basic Assert
val (_, userRecord) = parseTokenAndAssertBasics(jsonResponse, testData.token)

// Assert Expiration
if (testData.expectedTimeDeltaSeconds != null) {
assertExpirationTime(userRecord, currentTime, testData.expectedTimeDeltaSeconds)
}
}

// Helper method for exception testing
private fun parseTokenExpectingException(jsonResponse: String) {
val parser = ConnectTokenResponseParser<AuthInfo.TokenAuth>()
val userRecord = ConnectUserRecord()
val inputStream = ByteArrayInputStream(jsonResponse.toByteArray())
parser.parse(200, inputStream, userRecord)
}

@Test
fun testParseValidResponseWithExpiresIn() {
testTokenWithExpiration(
TokenTestData(
token = "test_token_123",
expiresInSeconds = 3600,
expectedTimeDeltaSeconds = 3600L,
),
)
}

@Test
fun testParseValidResponseWithoutExpiresIn() {
testTokenWithExpiration(
TokenTestData(
token = "another_test_token",
expiresInSeconds = null,
expectedTimeDeltaSeconds = 0L,
),
)
}

@Test
fun testParseValidResponseWithZeroExpiresIn() {
testTokenWithExpiration(
TokenTestData(
token = "zero_expiry_token",
expiresInSeconds = 0,
expectedTimeDeltaSeconds = 0L,
),
)
}

@Test(expected = IllegalStateException::class)
fun testParseEmptyToken() {
testTokenWithExpiration(
TokenTestData(
token = "",
expiresInSeconds = 0,
expectedTimeDeltaSeconds = 0L,
),
)
}

@Test(expected = RuntimeException::class)
fun testParseInvalidJsonThrowsRuntimeException() {
// Arrange & Act & Assert
parseTokenExpectingException("{ invalid json }")
}

@Test(expected = RuntimeException::class)
fun testParseMissingTokenFieldThrowsRuntimeException() {
// Arrange
val jsonResponse =
"""
{
"${ConnectConstants.CONNECT_KEY_EXPIRES}": 3600
}
""".trimIndent()

// Act & Assert
parseTokenExpectingException(jsonResponse)
}

@Test(expected = IllegalStateException::class)
fun testParseNullTokenField() {
// Arrange
val jsonResponse = createTokenJson("null", 3600)

// Act & Assert
parseTokenExpectingException(jsonResponse)
}

@Test
fun testParseWithInvalidExpiresInType() {
// Arrange
val jsonResponse = createTokenJson("test_token", "\"not_a_number\"")

// Act & Assert
// optInt returns 0 for invalid strings, so this should work without throwing exception
val (_, userRecord) = parseTokenAndAssertBasics(jsonResponse, "test_token")
}

@Test(expected = IllegalStateException::class)
fun testParseWithNegativeExpiresIn() {
// Arrange
val jsonResponse = createTokenJson("test_token", -3600)

// Act & Assert
parseTokenExpectingException(jsonResponse)
}

@Test(expected = RuntimeException::class)
fun testParseEmptyResponseThrowsRuntimeException() {
// Arrange & Act & Assert
parseTokenExpectingException("")
}
}
Loading