Skip to content

Improved fallback mechanism when user is not using hilt/dagger #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 3, 2025
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
74 changes: 16 additions & 58 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
run: ./gradlew assemble

- name: Save Build Artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: app-build
path: app/build/outputs/apk/
Expand Down Expand Up @@ -76,61 +76,19 @@ jobs:
- name: Run Unit Tests and Capture Results
id: run_tests
run: |
# Run tests and capture exit code
./gradlew test | tee test-results.txt
TEST_RESULTS=$(grep -A 7 "Test Summary" test-results.txt | tail -n +2 | sed 's/\x1B\[[0-9;]*[JKmsu]//g' | tr -d '[:space:]')
echo "$TEST_RESULTS" > test-summary.txt

# - name: Format Test Results for Two-Row Markdown Table with Emojis
# run: |
# echo "### Test Results" > formatted-summary.txt
# echo "" >> formatted-summary.txt
# echo "| Total Tests | Passed | Failed | Skipped | Result |" >> formatted-summary.txt
# echo "|-------------|--------|--------|---------|--------|" >> formatted-summary.txt

# echo "Raw Test Summary Content:"
# cat test-summary.txt

# # Extracting values using awk
# TOTAL=$(echo "$TEST_RESULTS" | awk -F'[: ]+' '/TotalTests/ {print $2}')
# PASSED=$(echo "$TEST_RESULTS" | awk -F'[: ]+' '/Passed/ {print $2}')
# FAILED=$(echo "$TEST_RESULTS" | awk -F'[: ]+' '/Failed/ {print $2}')
# SKIPPED=$(echo "$TEST_RESULTS" | awk -F'[: ]+' '/Skipped/ {print $2}')
# RESULT=$(echo "$TEST_RESULTS" | awk -F'[: ]+' '/Result/ {print $2}')

# # Detailed debugging
# echo "Debug: Extracted Values:"
# echo " TOTAL: '$TOTAL'"
# echo " PASSED: '$PASSED'"
# echo " FAILED: '$FAILED'"
# echo " SKIPPED: '$SKIPPED'"
# echo " RESULT (Raw): '$RESULT'"

# if [[ "$RESULT" == "SUCCESS" ]]; then
# EMOJI="✅"
# elif [[ "$RESULT" == "FAILURE" ]]; then
# EMOJI="❌"
# else
# EMOJI="⚠️"
# fi

# echo "Debug: Final RESULT after processing: '$RESULT $EMOJI'"

# echo "| $TOTAL | $PASSED | $FAILED | $SKIPPED | $RESULT $EMOJI |" >> formatted-summary.txt

# # Show the final output for debugging
# echo "Final formatted-summary.txt content:"
# cat formatted-summary.txt

# - name: Comment on PR
# if: github.event_name == 'pull_request'
# uses: actions/github-script@v6
# with:
# script: |
# const fs = require('fs');
# const testResults = fs.readFileSync('formatted-summary.txt', 'utf8');
# github.rest.issues.createComment({
# issue_number: context.issue.number,
# owner: context.repo.owner,
# repo: context.repo.repo,
# body: testResults
# });
TEST_EXIT_CODE=${PIPESTATUS[0]} # Capture the exit code of ./gradlew test

# Extract a clean test summary from Gradle output
echo "### 📌 Unit Test Summary" > test-summary.txt
grep -A 10 "Test Summary" test-results.txt | tail -n +2 | sed 's/\x1B\[[0-9;]*[JKmsu]//g' >> test-summary.txt

# Print the test summary in GitHub Actions logs
cat test-summary.txt

# Fail the workflow if tests fail
if [ $TEST_EXIT_CODE -ne 0 ]; then
echo "❌ Unit tests failed. See test-results.txt for details."
exit $TEST_EXIT_CODE # Fail the workflow
fi
4 changes: 2 additions & 2 deletions .github/workflows/semgrep.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:

- name: Upload Semgrep results
if: always()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: semgrep-results
path: semgrep.sarif
path: semgrep.sarif
4 changes: 2 additions & 2 deletions chat-sdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ dependencies {
implementation(libs.loggingInterceptor)

//Hilt
implementation(libs.hiltAndroid)
implementation(libs.lifecycleProcess)
compileOnly(libs.hiltAndroid)
compileOnly(libs.lifecycleProcess)
kapt(libs.hiltCompiler)
kapt(libs.hiltAndroidCompiler)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,32 +36,50 @@ object ChatSessionProvider {
}
}

// Private method to initialize ChatSession using Hilt or manual fallback
private fun initializeChatSession(context: Context): ChatSession {
return if (isHiltAvailable()) {
// Use Hilt's EntryPoint mechanism if Hilt is available
val entryPoint = EntryPointAccessors.fromApplication(
context.applicationContext,
ChatModule.ChatSessionEntryPoint::class.java
)
entryPoint.getChatSession()
} else {
// Fallback to manual initialization
createChatSessionManually(context)
}
}

// Method to check if Hilt is available
private fun isHiltAvailable(): Boolean {
return try {
Class.forName("dagger.hilt.EntryPoints")
// Check for the presence of Hilt's core class
val hiltClass = Class.forName("dagger.hilt.android.EntryPointAccessors")
SDKLogger.logger.logDebug { "Hilt detected: $hiltClass" }
true
} catch (e: ClassNotFoundException) {
SDKLogger.logger.logDebug {"Hilt is not available"}
SDKLogger.logger.logDebug { "Hilt is not available: ${e.message}" }
false
} catch (e: Exception) {
// Catch any unexpected issues during class detection
SDKLogger.logger.logDebug { "Error while checking Hilt availability: ${e.message}" }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this is logged as Debug not err ?

false
}
}

private fun initializeChatSession(context: Context): ChatSession {
return try {
if (isHiltAvailable()) {
SDKLogger.logger.logDebug { "Attempting Hilt-based ChatSession initialization." }
val entryPoint = EntryPointAccessors.fromApplication(
context.applicationContext,
ChatModule.ChatSessionEntryPoint::class.java
)
entryPoint.getChatSession().also {
SDKLogger.logger.logDebug { "ChatSession initialized using Hilt." }
}
} else {
SDKLogger.logger.logDebug { "Hilt not available. Falling back to manual initialization." }
createChatSessionManually(context).also {
SDKLogger.logger.logDebug { "ChatSession initialized manually." }
}
}
} catch (e: Exception) {
// Handle unexpected errors during initialization
SDKLogger.logger.logDebug { "Error initializing ChatSession: ${e.message}" }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this as debug statement correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we aim to print it as a debug which won't interrupt the flow

SDKLogger.logger.logDebug { "Falling back to manual initialization due to error." }
createChatSessionManually(context).also {
SDKLogger.logger.logDebug { "ChatSession initialized manually after Hilt failure." }
}
}
}

// Manual initialization of ChatSession for non-Hilt users
private fun createChatSessionManually(context: Context): ChatSession {
val appContext = context.applicationContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import com.amazonaws.services.connectparticipant.model.StartAttachmentUploadRequ
import com.amazonaws.services.connectparticipant.model.StartAttachmentUploadResult
import com.amazonaws.services.connectparticipant.model.UploadMetadata
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import okhttp3.Response
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyMap
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.anyString
import org.mockito.Mockito.doAnswer
Expand All @@ -39,6 +42,8 @@ import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.net.URL
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import retrofit2.Response as RetrofitResponse

@ExperimentalCoroutinesApi
Expand Down Expand Up @@ -97,6 +102,10 @@ class AttachmentsManagerTest {

@Test
fun test_sendAttachment_success() = runTest {
// Create a latch that will be counted down when completeAttachmentUpload is invoked.
val latch = CountDownLatch(1)

// Stub the methods that sendAttachment uses.
doReturn(mockStartAttachmentUploadRequest)
.`when`(attachmentsManager)
.createStartAttachmentUploadRequest(mockConnectionToken, mockUri)
Expand All @@ -105,17 +114,29 @@ class AttachmentsManagerTest {
.`when`(awsClient)
.startAttachmentUpload(mockConnectionToken, mockStartAttachmentUploadRequest)

doReturn(Unit).`when`(attachmentsManager).completeAttachmentUpload(mockConnectionToken, mockAttachmentId)
// Stub completeAttachmentUpload so that when it is eventually called, we count down the latch.
doAnswer {
latch.countDown()
}.`when`(attachmentsManager).completeAttachmentUpload(mockConnectionToken, mockAttachmentId)

// Stub apiClient.uploadAttachment to trigger its callback with a "successful" response.
doAnswer { invocation ->
val callback = invocation.getArgument<((RetrofitResponse<*>?) -> Unit)>(3)
// The lambda is the 4th parameter.
val callback = invocation.getArgument<(RetrofitResponse<*>?) -> Unit>(3)
val mockResponse = mock(RetrofitResponse::class.java)
// Simulate a successful response.
`when`(mockResponse.isSuccessful).thenReturn(true)
callback(mockResponse)
null
}.`when`(apiClient).uploadAttachment(anyString(), anyMap(), anyOrNull(), anyOrNull())

// When: call sendAttachment. (Make sure attachmentsManager is a spy so that we can verify internal calls.)
attachmentsManager.sendAttachment(mockConnectionToken, mockUri)

// Wait for up to 1 second for completeAttachmentUpload to be invoked.
latch.await(1, TimeUnit.SECONDS)

// Then: verify the expected interactions.
verify(awsClient).startAttachmentUpload(anyString(), anyOrNull())
verify(apiClient).uploadAttachment(anyString(), anyMap(), anyOrNull(), anyOrNull())
verify(attachmentsManager).completeAttachmentUpload(mockConnectionToken, mockAttachmentId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,38 +141,73 @@ class ChatServiceImplTest {

@Test
fun test_disconnectParticipantConnection_success() = runTest {
// Simulate that the chat session is active.
`when`(connectionDetailsProvider.isChatSessionActive()).thenReturn(true)
val mockConnectionDetails = createMockConnectionDetails("valid_token")
`when`(connectionDetailsProvider.getConnectionDetails()).thenReturn(mockConnectionDetails)
`when`(awsClient.disconnectParticipantConnection(mockConnectionDetails.connectionToken)).thenReturn(Result.success(
DisconnectParticipantResult()
))
`when`(awsClient.disconnectParticipantConnection(mockConnectionDetails.connectionToken))
.thenReturn(Result.success(DisconnectParticipantResult()))

// Invoke the disconnection logic.
val result = chatService.disconnectChatSession()

// Verify that the overall result is successful.
assertTrue(result.isSuccess)
assertEquals(true, result.getOrNull())

// Verify that the proper calls were made.
verify(connectionDetailsProvider).isChatSessionActive()
verify(connectionDetailsProvider).getConnectionDetails()
verify(awsClient).disconnectParticipantConnection(mockConnectionDetails.connectionToken)
verify(webSocketManager).disconnect("Customer ended the chat")
verify(connectionDetailsProvider).setChatSessionState(false)
}

@Test
fun test_disconnectParticipantConnection_failure() = runTest {
// Simulate that the chat session is active.
`when`(connectionDetailsProvider.isChatSessionActive()).thenReturn(true)
val mockConnectionDetails = createMockConnectionDetails("invalid_token")
`when`(connectionDetailsProvider.getConnectionDetails()).thenReturn(mockConnectionDetails)
`when`(awsClient.disconnectParticipantConnection(mockConnectionDetails.connectionToken)).thenThrow(RuntimeException("Network error"))
// Simulate an error during AWS disconnect.
`when`(awsClient.disconnectParticipantConnection(mockConnectionDetails.connectionToken))
.thenThrow(RuntimeException("Network error"))

// Invoke the disconnection logic.
val result = chatService.disconnectChatSession()

// Verify that the overall result is a failure.
assertTrue(result.isFailure)

// Verify that the calls were attempted.
verify(connectionDetailsProvider).isChatSessionActive()
verify(connectionDetailsProvider).getConnectionDetails()
verify(awsClient).disconnectParticipantConnection(mockConnectionDetails.connectionToken)

// Because AWS disconnect failed, the subsequent steps should not be executed.
verify(webSocketManager, never()).disconnect("Customer ended the chat")
verify(connectionDetailsProvider, never()).setChatSessionState(false)
}

@Test
fun test_disconnectParticipantConnection_noConnectionDetails() = runTest {
// Simulate that the chat session is active.
`when`(connectionDetailsProvider.isChatSessionActive()).thenReturn(true)
// No connection details available.
`when`(connectionDetailsProvider.getConnectionDetails()).thenReturn(null)

// Invoke the disconnection logic.
val result = chatService.disconnectChatSession()

// Verify that the overall result is a failure.
assertTrue(result.isFailure)

// Verify that after checking the session state, it tried to get connection details.
verify(connectionDetailsProvider).isChatSessionActive()
verify(connectionDetailsProvider).getConnectionDetails()
// Because there are no connection details, AWS disconnect should never be called.
verify(awsClient, never()).disconnectParticipantConnection(anyString())
verify(webSocketManager, never()).disconnect("Customer ended the chat")
}

@Test
Expand Down
2 changes: 1 addition & 1 deletion chat-sdk/version.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
sdkVersion=1.0.5
sdkVersion=1.0.6
groupId=software.aws.connect
artifactId=amazon-connect-chat-android
Loading