@@ -5,12 +5,15 @@ package software.aws.toolkits.jetbrains.services.amazonq.clients
5
5
6
6
import com.intellij.testFramework.RuleChain
7
7
import com.intellij.testFramework.replaceService
8
+ import kotlinx.coroutines.runBlocking
8
9
import kotlinx.coroutines.test.runTest
10
+ import org.assertj.core.api.Assertions.assertThat
9
11
import org.junit.Before
10
12
import org.junit.Rule
11
13
import org.junit.Test
12
14
import org.mockito.kotlin.any
13
15
import org.mockito.kotlin.argumentCaptor
16
+ import org.mockito.kotlin.doAnswer
14
17
import org.mockito.kotlin.doReturn
15
18
import org.mockito.kotlin.mock
16
19
import org.mockito.kotlin.stub
@@ -20,6 +23,7 @@ import software.amazon.awssdk.services.codewhispererstreaming.CodeWhispererStrea
20
23
import software.amazon.awssdk.services.codewhispererstreaming.model.ExportIntent
21
24
import software.amazon.awssdk.services.codewhispererstreaming.model.ExportResultArchiveRequest
22
25
import software.amazon.awssdk.services.codewhispererstreaming.model.ExportResultArchiveResponseHandler
26
+ import software.amazon.awssdk.services.codewhispererstreaming.model.ValidationException
23
27
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
24
28
import software.aws.toolkits.core.TokenConnectionSettings
25
29
import software.aws.toolkits.core.utils.test.aString
@@ -81,4 +85,156 @@ class AmazonQStreamingClientTest : AmazonQTestBase() {
81
85
verify(streamingBearerClient).exportResultArchive(requestCaptor.capture(), handlerCaptor.capture())
82
86
}
83
87
}
88
+
89
+ @Test
90
+ fun `verify retry on ValidationException` (): Unit = runBlocking {
91
+ var attemptCount = 0
92
+ streamingBearerClient = mockClientManagerRule.create<CodeWhispererStreamingAsyncClient >().stub {
93
+ on {
94
+ exportResultArchive(any<ExportResultArchiveRequest >(), any<ExportResultArchiveResponseHandler >())
95
+ } doAnswer {
96
+ attemptCount++
97
+ if (attemptCount <= 2 ) {
98
+ CompletableFuture .runAsync {
99
+ throw VALIDATION_EXCEPTION
100
+ }
101
+ } else {
102
+ CompletableFuture .completedFuture(mock())
103
+ }
104
+ }
105
+ }
106
+
107
+ amazonQStreamingClient.exportResultArchive(" test-id" , ExportIntent .TRANSFORMATION , null , {}, {})
108
+
109
+ assertThat(attemptCount).isEqualTo(3 )
110
+ }
111
+
112
+ @Test
113
+ fun `verify retry gives up after max attempts` (): Unit = runBlocking {
114
+ var attemptCount = 0
115
+ streamingBearerClient = mockClientManagerRule.create<CodeWhispererStreamingAsyncClient >().stub {
116
+ on {
117
+ exportResultArchive(any<ExportResultArchiveRequest >(), any<ExportResultArchiveResponseHandler >())
118
+ } doAnswer {
119
+ attemptCount++
120
+ CompletableFuture .runAsync {
121
+ throw VALIDATION_EXCEPTION
122
+ }
123
+ }
124
+ }
125
+
126
+ val thrown = catchCoroutineException {
127
+ amazonQStreamingClient.exportResultArchive(" test-id" , ExportIntent .TRANSFORMATION , null , {}, {})
128
+ }
129
+
130
+ assertThat(attemptCount).isEqualTo(3 )
131
+ assertThat(thrown)
132
+ .isInstanceOf(ValidationException ::class .java)
133
+ .hasMessage(" Resource validation failed" )
134
+ }
135
+
136
+ @Test
137
+ fun `verify no retry on non-retryable exception` (): Unit = runBlocking {
138
+ var attemptCount = 0
139
+
140
+ streamingBearerClient = mockClientManagerRule.create<CodeWhispererStreamingAsyncClient >().stub {
141
+ on {
142
+ exportResultArchive(any<ExportResultArchiveRequest >(), any<ExportResultArchiveResponseHandler >())
143
+ } doAnswer {
144
+ attemptCount++
145
+ CompletableFuture .runAsync {
146
+ throw IllegalArgumentException (" Non-retryable error" )
147
+ }
148
+ }
149
+ }
150
+
151
+ val thrown = catchCoroutineException {
152
+ amazonQStreamingClient.exportResultArchive(" test-id" , ExportIntent .TRANSFORMATION , null , {}, {})
153
+ }
154
+
155
+ assertThat(attemptCount).isEqualTo(1 )
156
+ assertThat(thrown)
157
+ .isInstanceOf(IllegalArgumentException ::class .java)
158
+ .hasMessage(" Non-retryable error" )
159
+ }
160
+
161
+ @Test
162
+ fun `verify backoff timing between retries` (): Unit = runBlocking {
163
+ var lastAttemptTime = 0L
164
+ var minBackoffObserved = Long .MAX_VALUE
165
+ var maxBackoffObserved = 0L
166
+
167
+ streamingBearerClient = mockClientManagerRule.create<CodeWhispererStreamingAsyncClient >().stub {
168
+ on {
169
+ exportResultArchive(any<ExportResultArchiveRequest >(), any<ExportResultArchiveResponseHandler >())
170
+ } doAnswer {
171
+ val currentTime = System .currentTimeMillis()
172
+ if (lastAttemptTime > 0 ) {
173
+ val backoffTime = currentTime - lastAttemptTime
174
+ minBackoffObserved = minOf(minBackoffObserved, backoffTime)
175
+ maxBackoffObserved = maxOf(maxBackoffObserved, backoffTime)
176
+ }
177
+ lastAttemptTime = currentTime
178
+
179
+ CompletableFuture .runAsync {
180
+ throw VALIDATION_EXCEPTION
181
+ }
182
+ }
183
+ }
184
+
185
+ val thrown = catchCoroutineException {
186
+ amazonQStreamingClient.exportResultArchive(" test-id" , ExportIntent .TRANSFORMATION , null , {}, {})
187
+ }
188
+
189
+ assertThat(thrown)
190
+ .isInstanceOf(ValidationException ::class .java)
191
+ .hasMessage(" Resource validation failed" )
192
+ assertThat(minBackoffObserved).isGreaterThanOrEqualTo(100 )
193
+ assertThat(maxBackoffObserved).isLessThanOrEqualTo(10000 )
194
+ }
195
+
196
+ @Test
197
+ fun `verify onError callback is called with final exception` (): Unit = runBlocking {
198
+ var errorCaught: Exception ? = null
199
+
200
+ streamingBearerClient = mockClientManagerRule.create<CodeWhispererStreamingAsyncClient >().stub {
201
+ on {
202
+ exportResultArchive(any<ExportResultArchiveRequest >(), any<ExportResultArchiveResponseHandler >())
203
+ } doAnswer {
204
+ CompletableFuture .runAsync {
205
+ throw VALIDATION_EXCEPTION
206
+ }
207
+ }
208
+ }
209
+
210
+ val thrown = catchCoroutineException {
211
+ amazonQStreamingClient.exportResultArchive(
212
+ " test-id" ,
213
+ ExportIntent .TRANSFORMATION ,
214
+ null ,
215
+ { errorCaught = it },
216
+ {}
217
+ )
218
+ }
219
+
220
+ assertThat(thrown)
221
+ .isInstanceOf(ValidationException ::class .java)
222
+ .hasMessage(" Resource validation failed" )
223
+ assertThat(errorCaught).isEqualTo(VALIDATION_EXCEPTION )
224
+ }
225
+
226
+ private suspend fun catchCoroutineException (block : suspend () -> Unit ): Throwable {
227
+ try {
228
+ block()
229
+ error(" Expected exception was not thrown" )
230
+ } catch (e: Throwable ) {
231
+ return e
232
+ }
233
+ }
234
+
235
+ companion object {
236
+ private val VALIDATION_EXCEPTION = ValidationException .builder()
237
+ .message(" Resource validation failed" )
238
+ .build()
239
+ }
84
240
}
0 commit comments