Skip to content

Commit 18274c7

Browse files
authored
Improved fallback mechanism when user is not using hilt/dagger (#43)
* Improved fallback mechanism when user is not using hilt/dagger * Update ci.yml with artifact version * fixed failing unit tests * Update semgrep.yaml
1 parent 76915ec commit 18274c7

File tree

7 files changed

+118
-86
lines changed

7 files changed

+118
-86
lines changed

.github/workflows/ci.yml

Lines changed: 16 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
run: ./gradlew assemble
4040

4141
- name: Save Build Artifacts
42-
uses: actions/upload-artifact@v3
42+
uses: actions/upload-artifact@v4
4343
with:
4444
name: app-build
4545
path: app/build/outputs/apk/
@@ -76,61 +76,19 @@ jobs:
7676
- name: Run Unit Tests and Capture Results
7777
id: run_tests
7878
run: |
79+
# Run tests and capture exit code
7980
./gradlew test | tee test-results.txt
80-
TEST_RESULTS=$(grep -A 7 "Test Summary" test-results.txt | tail -n +2 | sed 's/\x1B\[[0-9;]*[JKmsu]//g' | tr -d '[:space:]')
81-
echo "$TEST_RESULTS" > test-summary.txt
82-
83-
# - name: Format Test Results for Two-Row Markdown Table with Emojis
84-
# run: |
85-
# echo "### Test Results" > formatted-summary.txt
86-
# echo "" >> formatted-summary.txt
87-
# echo "| Total Tests | Passed | Failed | Skipped | Result |" >> formatted-summary.txt
88-
# echo "|-------------|--------|--------|---------|--------|" >> formatted-summary.txt
89-
90-
# echo "Raw Test Summary Content:"
91-
# cat test-summary.txt
92-
93-
# # Extracting values using awk
94-
# TOTAL=$(echo "$TEST_RESULTS" | awk -F'[: ]+' '/TotalTests/ {print $2}')
95-
# PASSED=$(echo "$TEST_RESULTS" | awk -F'[: ]+' '/Passed/ {print $2}')
96-
# FAILED=$(echo "$TEST_RESULTS" | awk -F'[: ]+' '/Failed/ {print $2}')
97-
# SKIPPED=$(echo "$TEST_RESULTS" | awk -F'[: ]+' '/Skipped/ {print $2}')
98-
# RESULT=$(echo "$TEST_RESULTS" | awk -F'[: ]+' '/Result/ {print $2}')
99-
100-
# # Detailed debugging
101-
# echo "Debug: Extracted Values:"
102-
# echo " TOTAL: '$TOTAL'"
103-
# echo " PASSED: '$PASSED'"
104-
# echo " FAILED: '$FAILED'"
105-
# echo " SKIPPED: '$SKIPPED'"
106-
# echo " RESULT (Raw): '$RESULT'"
107-
108-
# if [[ "$RESULT" == "SUCCESS" ]]; then
109-
# EMOJI="✅"
110-
# elif [[ "$RESULT" == "FAILURE" ]]; then
111-
# EMOJI="❌"
112-
# else
113-
# EMOJI="⚠️"
114-
# fi
115-
116-
# echo "Debug: Final RESULT after processing: '$RESULT $EMOJI'"
117-
118-
# echo "| $TOTAL | $PASSED | $FAILED | $SKIPPED | $RESULT $EMOJI |" >> formatted-summary.txt
119-
120-
# # Show the final output for debugging
121-
# echo "Final formatted-summary.txt content:"
122-
# cat formatted-summary.txt
123-
124-
# - name: Comment on PR
125-
# if: github.event_name == 'pull_request'
126-
# uses: actions/github-script@v6
127-
# with:
128-
# script: |
129-
# const fs = require('fs');
130-
# const testResults = fs.readFileSync('formatted-summary.txt', 'utf8');
131-
# github.rest.issues.createComment({
132-
# issue_number: context.issue.number,
133-
# owner: context.repo.owner,
134-
# repo: context.repo.repo,
135-
# body: testResults
136-
# });
81+
TEST_EXIT_CODE=${PIPESTATUS[0]} # Capture the exit code of ./gradlew test
82+
83+
# Extract a clean test summary from Gradle output
84+
echo "### 📌 Unit Test Summary" > test-summary.txt
85+
grep -A 10 "Test Summary" test-results.txt | tail -n +2 | sed 's/\x1B\[[0-9;]*[JKmsu]//g' >> test-summary.txt
86+
87+
# Print the test summary in GitHub Actions logs
88+
cat test-summary.txt
89+
90+
# Fail the workflow if tests fail
91+
if [ $TEST_EXIT_CODE -ne 0 ]; then
92+
echo "❌ Unit tests failed. See test-results.txt for details."
93+
exit $TEST_EXIT_CODE # Fail the workflow
94+
fi

.github/workflows/semgrep.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
- name: Upload Semgrep results
2727
if: always()
28-
uses: actions/upload-artifact@v3
28+
uses: actions/upload-artifact@v4
2929
with:
3030
name: semgrep-results
31-
path: semgrep.sarif
31+
path: semgrep.sarif

chat-sdk/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ dependencies {
8181
implementation(libs.loggingInterceptor)
8282

8383
//Hilt
84-
implementation(libs.hiltAndroid)
85-
implementation(libs.lifecycleProcess)
84+
compileOnly(libs.hiltAndroid)
85+
compileOnly(libs.lifecycleProcess)
8686
kapt(libs.hiltCompiler)
8787
kapt(libs.hiltAndroidCompiler)
8888

chat-sdk/src/main/java/com/amazon/connect/chat/sdk/provider/ChatSessionProvider.kt

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,32 +36,50 @@ object ChatSessionProvider {
3636
}
3737
}
3838

39-
// Private method to initialize ChatSession using Hilt or manual fallback
40-
private fun initializeChatSession(context: Context): ChatSession {
41-
return if (isHiltAvailable()) {
42-
// Use Hilt's EntryPoint mechanism if Hilt is available
43-
val entryPoint = EntryPointAccessors.fromApplication(
44-
context.applicationContext,
45-
ChatModule.ChatSessionEntryPoint::class.java
46-
)
47-
entryPoint.getChatSession()
48-
} else {
49-
// Fallback to manual initialization
50-
createChatSessionManually(context)
51-
}
52-
}
53-
5439
// Method to check if Hilt is available
5540
private fun isHiltAvailable(): Boolean {
5641
return try {
57-
Class.forName("dagger.hilt.EntryPoints")
42+
// Check for the presence of Hilt's core class
43+
val hiltClass = Class.forName("dagger.hilt.android.EntryPointAccessors")
44+
SDKLogger.logger.logDebug { "Hilt detected: $hiltClass" }
5845
true
5946
} catch (e: ClassNotFoundException) {
60-
SDKLogger.logger.logDebug {"Hilt is not available"}
47+
SDKLogger.logger.logDebug { "Hilt is not available: ${e.message}" }
48+
false
49+
} catch (e: Exception) {
50+
// Catch any unexpected issues during class detection
51+
SDKLogger.logger.logDebug { "Error while checking Hilt availability: ${e.message}" }
6152
false
6253
}
6354
}
6455

56+
private fun initializeChatSession(context: Context): ChatSession {
57+
return try {
58+
if (isHiltAvailable()) {
59+
SDKLogger.logger.logDebug { "Attempting Hilt-based ChatSession initialization." }
60+
val entryPoint = EntryPointAccessors.fromApplication(
61+
context.applicationContext,
62+
ChatModule.ChatSessionEntryPoint::class.java
63+
)
64+
entryPoint.getChatSession().also {
65+
SDKLogger.logger.logDebug { "ChatSession initialized using Hilt." }
66+
}
67+
} else {
68+
SDKLogger.logger.logDebug { "Hilt not available. Falling back to manual initialization." }
69+
createChatSessionManually(context).also {
70+
SDKLogger.logger.logDebug { "ChatSession initialized manually." }
71+
}
72+
}
73+
} catch (e: Exception) {
74+
// Handle unexpected errors during initialization
75+
SDKLogger.logger.logDebug { "Error initializing ChatSession: ${e.message}" }
76+
SDKLogger.logger.logDebug { "Falling back to manual initialization due to error." }
77+
createChatSessionManually(context).also {
78+
SDKLogger.logger.logDebug { "ChatSession initialized manually after Hilt failure." }
79+
}
80+
}
81+
}
82+
6583
// Manual initialization of ChatSession for non-Hilt users
6684
private fun createChatSessionManually(context: Context): ChatSession {
6785
val appContext = context.applicationContext

chat-sdk/src/test/java/com/amazon/connect/chat/sdk/network/AttachmentsManagerTest.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ import com.amazonaws.services.connectparticipant.model.StartAttachmentUploadRequ
1616
import com.amazonaws.services.connectparticipant.model.StartAttachmentUploadResult
1717
import com.amazonaws.services.connectparticipant.model.UploadMetadata
1818
import junit.framework.TestCase.assertTrue
19+
import junit.framework.TestCase.fail
1920
import kotlinx.coroutines.ExperimentalCoroutinesApi
21+
import kotlinx.coroutines.launch
2022
import kotlinx.coroutines.test.runTest
2123
import okhttp3.Response
2224
import org.junit.Before
2325
import org.junit.Test
2426
import org.junit.runner.RunWith
2527
import org.mockito.ArgumentMatchers.anyMap
28+
import org.mockito.ArgumentMatchers.eq
2629
import org.mockito.Mock
2730
import org.mockito.Mockito.anyString
2831
import org.mockito.Mockito.doAnswer
@@ -39,6 +42,8 @@ import java.io.File
3942
import java.io.FileInputStream
4043
import java.io.InputStream
4144
import java.net.URL
45+
import java.util.concurrent.CountDownLatch
46+
import java.util.concurrent.TimeUnit
4247
import retrofit2.Response as RetrofitResponse
4348

4449
@ExperimentalCoroutinesApi
@@ -97,6 +102,10 @@ class AttachmentsManagerTest {
97102

98103
@Test
99104
fun test_sendAttachment_success() = runTest {
105+
// Create a latch that will be counted down when completeAttachmentUpload is invoked.
106+
val latch = CountDownLatch(1)
107+
108+
// Stub the methods that sendAttachment uses.
100109
doReturn(mockStartAttachmentUploadRequest)
101110
.`when`(attachmentsManager)
102111
.createStartAttachmentUploadRequest(mockConnectionToken, mockUri)
@@ -105,17 +114,29 @@ class AttachmentsManagerTest {
105114
.`when`(awsClient)
106115
.startAttachmentUpload(mockConnectionToken, mockStartAttachmentUploadRequest)
107116

108-
doReturn(Unit).`when`(attachmentsManager).completeAttachmentUpload(mockConnectionToken, mockAttachmentId)
117+
// Stub completeAttachmentUpload so that when it is eventually called, we count down the latch.
118+
doAnswer {
119+
latch.countDown()
120+
}.`when`(attachmentsManager).completeAttachmentUpload(mockConnectionToken, mockAttachmentId)
109121

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

133+
// When: call sendAttachment. (Make sure attachmentsManager is a spy so that we can verify internal calls.)
118134
attachmentsManager.sendAttachment(mockConnectionToken, mockUri)
135+
136+
// Wait for up to 1 second for completeAttachmentUpload to be invoked.
137+
latch.await(1, TimeUnit.SECONDS)
138+
139+
// Then: verify the expected interactions.
119140
verify(awsClient).startAttachmentUpload(anyString(), anyOrNull())
120141
verify(apiClient).uploadAttachment(anyString(), anyMap(), anyOrNull(), anyOrNull())
121142
verify(attachmentsManager).completeAttachmentUpload(mockConnectionToken, mockAttachmentId)

chat-sdk/src/test/java/com/amazon/connect/chat/sdk/repository/ChatServiceImplTest.kt

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,38 +141,73 @@ class ChatServiceImplTest {
141141

142142
@Test
143143
fun test_disconnectParticipantConnection_success() = runTest {
144+
// Simulate that the chat session is active.
145+
`when`(connectionDetailsProvider.isChatSessionActive()).thenReturn(true)
144146
val mockConnectionDetails = createMockConnectionDetails("valid_token")
145147
`when`(connectionDetailsProvider.getConnectionDetails()).thenReturn(mockConnectionDetails)
146-
`when`(awsClient.disconnectParticipantConnection(mockConnectionDetails.connectionToken)).thenReturn(Result.success(
147-
DisconnectParticipantResult()
148-
))
148+
`when`(awsClient.disconnectParticipantConnection(mockConnectionDetails.connectionToken))
149+
.thenReturn(Result.success(DisconnectParticipantResult()))
149150

151+
// Invoke the disconnection logic.
150152
val result = chatService.disconnectChatSession()
151153

154+
// Verify that the overall result is successful.
152155
assertTrue(result.isSuccess)
156+
assertEquals(true, result.getOrNull())
157+
158+
// Verify that the proper calls were made.
159+
verify(connectionDetailsProvider).isChatSessionActive()
153160
verify(connectionDetailsProvider).getConnectionDetails()
154161
verify(awsClient).disconnectParticipantConnection(mockConnectionDetails.connectionToken)
162+
verify(webSocketManager).disconnect("Customer ended the chat")
163+
verify(connectionDetailsProvider).setChatSessionState(false)
155164
}
156165

157166
@Test
158167
fun test_disconnectParticipantConnection_failure() = runTest {
168+
// Simulate that the chat session is active.
169+
`when`(connectionDetailsProvider.isChatSessionActive()).thenReturn(true)
159170
val mockConnectionDetails = createMockConnectionDetails("invalid_token")
160171
`when`(connectionDetailsProvider.getConnectionDetails()).thenReturn(mockConnectionDetails)
161-
`when`(awsClient.disconnectParticipantConnection(mockConnectionDetails.connectionToken)).thenThrow(RuntimeException("Network error"))
172+
// Simulate an error during AWS disconnect.
173+
`when`(awsClient.disconnectParticipantConnection(mockConnectionDetails.connectionToken))
174+
.thenThrow(RuntimeException("Network error"))
162175

176+
// Invoke the disconnection logic.
163177
val result = chatService.disconnectChatSession()
178+
179+
// Verify that the overall result is a failure.
164180
assertTrue(result.isFailure)
181+
182+
// Verify that the calls were attempted.
183+
verify(connectionDetailsProvider).isChatSessionActive()
165184
verify(connectionDetailsProvider).getConnectionDetails()
166185
verify(awsClient).disconnectParticipantConnection(mockConnectionDetails.connectionToken)
186+
187+
// Because AWS disconnect failed, the subsequent steps should not be executed.
188+
verify(webSocketManager, never()).disconnect("Customer ended the chat")
189+
verify(connectionDetailsProvider, never()).setChatSessionState(false)
167190
}
168191

169192
@Test
170193
fun test_disconnectParticipantConnection_noConnectionDetails() = runTest {
194+
// Simulate that the chat session is active.
195+
`when`(connectionDetailsProvider.isChatSessionActive()).thenReturn(true)
196+
// No connection details available.
171197
`when`(connectionDetailsProvider.getConnectionDetails()).thenReturn(null)
198+
199+
// Invoke the disconnection logic.
172200
val result = chatService.disconnectChatSession()
201+
202+
// Verify that the overall result is a failure.
173203
assertTrue(result.isFailure)
204+
205+
// Verify that after checking the session state, it tried to get connection details.
206+
verify(connectionDetailsProvider).isChatSessionActive()
174207
verify(connectionDetailsProvider).getConnectionDetails()
208+
// Because there are no connection details, AWS disconnect should never be called.
175209
verify(awsClient, never()).disconnectParticipantConnection(anyString())
210+
verify(webSocketManager, never()).disconnect("Customer ended the chat")
176211
}
177212

178213
@Test

chat-sdk/version.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
sdkVersion=1.0.5
1+
sdkVersion=1.0.6
22
groupId=software.aws.connect
33
artifactId=amazon-connect-chat-android

0 commit comments

Comments
 (0)