Skip to content

Commit f6d81ee

Browse files
authored
feat(data): Fix Data event parsing + unit tests (#3031)
1 parent ac4a7a4 commit f6d81ee

File tree

25 files changed

+1406
-35
lines changed

25 files changed

+1406
-35
lines changed

appsync/aws-appsync-amplify/src/test/java/com/amplifyframework/aws/appsync/core/authorizers/AmplifyIamAuthorizerTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package com.amplifyframework.aws.appsync.core.authorizers
1616

1717
import com.amplifyframework.aws.appsync.core.AppSyncRequest
1818
import com.amplifyframework.aws.appsync.core.util.AppSyncRequestSigner
19+
import io.kotest.assertions.throwables.shouldThrow
1920
import io.kotest.matchers.maps.shouldContainExactly
2021
import io.mockk.coEvery
2122
import io.mockk.mockk
@@ -38,4 +39,20 @@ class AmplifyIamAuthorizerTest {
3839

3940
authorizer.getAuthorizationHeaders(request) shouldContainExactly mapOf("Authorization" to "test-signature")
4041
}
42+
43+
@Test
44+
fun `iam authorizer throws if failed to fetch token from amplify`() = runTest {
45+
val request = mockk<AppSyncRequest>()
46+
val signer = mockk<AppSyncRequestSigner> {
47+
coEvery {
48+
signAppSyncRequest(request, region)
49+
} throws IllegalStateException()
50+
}
51+
52+
val authorizer = AmplifyIamAuthorizer(region, signer)
53+
54+
shouldThrow<IllegalStateException> {
55+
authorizer.getAuthorizationHeaders(request)
56+
}
57+
}
4158
}

appsync/aws-appsync-amplify/src/test/java/com/amplifyframework/aws/appsync/core/authorizers/AmplifyUserPoolAuthorizerTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package com.amplifyframework.aws.appsync.core.authorizers
1616

1717
import com.amplifyframework.auth.AuthCredentialsProvider
1818
import com.amplifyframework.core.Consumer
19+
import io.kotest.assertions.throwables.shouldThrow
1920
import io.kotest.matchers.maps.shouldContainExactly
2021
import io.mockk.CapturingSlot
2122
import io.mockk.every
@@ -39,4 +40,17 @@ class AmplifyUserPoolAuthorizerTest {
3940

4041
authorizer.getAuthorizationHeaders(mockk()) shouldContainExactly mapOf("Authorization" to expectedValue)
4142
}
43+
44+
@Test
45+
fun `user pool authorizer throws if failed to fetch token from amplify`() = runTest {
46+
val cognitoCredentialsProvider = mockk<AuthCredentialsProvider> {
47+
every { getAccessToken(any(), any()) } throws IllegalStateException()
48+
}
49+
val accessTokenProvider = AccessTokenProvider(cognitoCredentialsProvider)
50+
val authorizer = AmplifyUserPoolAuthorizer(accessTokenProvider)
51+
52+
shouldThrow<IllegalStateException> {
53+
authorizer.getAuthorizationHeaders(mockk())
54+
}
55+
}
4256
}

appsync/aws-appsync-amplify/src/test/java/com/amplifyframework/aws/appsync/core/util/AppSyncRequestSignerTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
/*
2+
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
115
package com.amplifyframework.aws.appsync.core.util
216

317
import aws.smithy.kotlin.runtime.InternalApi

appsync/aws-appsync-core/src/main/java/com/amplifyframework/aws/appsync/core/AppSyncRequest.kt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,4 @@ internal object HeaderKeys {
3333
const val AMAZON_DATE = "x-amz-date"
3434
const val API_KEY = "x-api-key"
3535
const val AUTHORIZATION = "Authorization"
36-
const val HOST = "host"
37-
const val ACCEPT = "accept"
38-
const val CONTENT_TYPE = "content-type"
39-
const val SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"
40-
}
41-
42-
internal object HeaderValues {
43-
const val ACCEPT_APPLICATION_JSON = "application/json, text/javascript"
44-
const val CONTENT_TYPE_APPLICATION_JSON = "application/json; charset=UTF-8"
45-
const val SEC_WEBSOCKET_PROTOCOL_APPSYNC_EVENTS = "aws-appsync-event-ws"
4636
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package com.amplifyframework.aws.appsync.core
16+
17+
import io.kotest.matchers.shouldBe
18+
import org.junit.Test
19+
20+
class AppSyncRequestTest {
21+
22+
@Test
23+
fun `test request implementation`() {
24+
val testRequest = object : AppSyncRequest {
25+
override val method = AppSyncRequest.HttpMethod.POST
26+
override val url = "https://amazon.com"
27+
override val headers = mapOf(
28+
HeaderKeys.API_KEY to "123",
29+
HeaderKeys.AUTHORIZATION to "345",
30+
HeaderKeys.AMAZON_DATE to "2025"
31+
)
32+
override val body = "b"
33+
}
34+
35+
testRequest.method shouldBe AppSyncRequest.HttpMethod.POST
36+
testRequest.url shouldBe "https://amazon.com"
37+
testRequest.headers shouldBe mapOf(
38+
HeaderKeys.API_KEY to "123",
39+
HeaderKeys.AUTHORIZATION to "345",
40+
HeaderKeys.AMAZON_DATE to "2025"
41+
)
42+
testRequest.body shouldBe "b"
43+
}
44+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amplifyframework.aws.appsync.core
17+
18+
import io.kotest.matchers.collections.shouldHaveSize
19+
import io.kotest.matchers.equals.shouldBeEqual
20+
import org.junit.Test
21+
22+
// We only need to test the suppliers as the other logs levels don't react to thresholds. That is up to the
23+
// class that implements Logger and writes the implementation for each log message type.
24+
class LoggerTest {
25+
26+
private val errorLog = "error"
27+
private val errorLogWithThrowable = Pair(errorLog, IllegalStateException())
28+
private val warnLog = "warn"
29+
private val warnLogWithThrowable = Pair(warnLog, IllegalStateException())
30+
private val infoLog = "info"
31+
private val debugLog = "debug"
32+
private val verboseLog = "verbose"
33+
34+
@Test
35+
fun `test suppliers with none threshold`() {
36+
val logger = TestSupplierLogger(LogLevel.NONE)
37+
38+
writeTestLogs(logger)
39+
40+
logger.warnLogs shouldHaveSize 0
41+
logger.warnLogs shouldHaveSize 0
42+
logger.infoLogs shouldHaveSize 0
43+
logger.debugLogs shouldHaveSize 0
44+
logger.verboseLogs shouldHaveSize 0
45+
}
46+
47+
@Test
48+
fun `test suppliers with error threshold`() {
49+
val logger = TestSupplierLogger(LogLevel.ERROR)
50+
51+
writeTestLogs(logger)
52+
53+
logger.errorLogs shouldBeEqual listOf(Pair(errorLog, null), errorLogWithThrowable)
54+
logger.warnLogs shouldHaveSize 0
55+
logger.infoLogs shouldHaveSize 0
56+
logger.debugLogs shouldHaveSize 0
57+
logger.verboseLogs shouldHaveSize 0
58+
}
59+
60+
@Test
61+
fun `test suppliers with warn threshold`() {
62+
val logger = TestSupplierLogger(LogLevel.WARN)
63+
64+
writeTestLogs(logger)
65+
66+
logger.errorLogs shouldBeEqual listOf(Pair(errorLog, null), errorLogWithThrowable)
67+
logger.warnLogs shouldBeEqual listOf(Pair(warnLog, null), warnLogWithThrowable)
68+
logger.infoLogs shouldHaveSize 0
69+
logger.debugLogs shouldHaveSize 0
70+
logger.verboseLogs shouldHaveSize 0
71+
}
72+
73+
@Test
74+
fun `test suppliers with info threshold`() {
75+
val logger = TestSupplierLogger(LogLevel.INFO)
76+
77+
writeTestLogs(logger)
78+
79+
logger.errorLogs shouldBeEqual listOf(Pair(errorLog, null), errorLogWithThrowable)
80+
logger.warnLogs shouldBeEqual listOf(Pair(warnLog, null), warnLogWithThrowable)
81+
logger.infoLogs shouldBeEqual listOf(infoLog)
82+
logger.debugLogs shouldHaveSize 0
83+
logger.verboseLogs shouldHaveSize 0
84+
}
85+
86+
@Test
87+
fun `test suppliers with debug threshold`() {
88+
val logger = TestSupplierLogger(LogLevel.DEBUG)
89+
90+
writeTestLogs(logger)
91+
92+
logger.errorLogs shouldBeEqual listOf(Pair(errorLog, null), errorLogWithThrowable)
93+
logger.warnLogs shouldBeEqual listOf(Pair(warnLog, null), warnLogWithThrowable)
94+
logger.infoLogs shouldBeEqual listOf(infoLog)
95+
logger.debugLogs shouldBeEqual listOf(debugLog)
96+
logger.verboseLogs shouldHaveSize 0
97+
}
98+
99+
@Test
100+
fun `test suppliers with verbose threshold`() {
101+
val logger = TestSupplierLogger(LogLevel.VERBOSE)
102+
103+
writeTestLogs(logger)
104+
105+
logger.errorLogs shouldBeEqual listOf(Pair(errorLog, null), errorLogWithThrowable)
106+
logger.warnLogs shouldBeEqual listOf(Pair(warnLog, null), warnLogWithThrowable)
107+
logger.infoLogs shouldBeEqual listOf(infoLog)
108+
logger.debugLogs shouldBeEqual listOf(debugLog)
109+
logger.verboseLogs shouldBeEqual listOf(verboseLog)
110+
}
111+
112+
private fun writeTestLogs(logger: Logger) {
113+
logger.error { errorLog }
114+
logger.error(errorLogWithThrowable.second) { errorLogWithThrowable.first }
115+
logger.warn { warnLog }
116+
logger.warn(warnLogWithThrowable.second) { warnLogWithThrowable.first }
117+
logger.info { infoLog }
118+
logger.debug { debugLog }
119+
logger.verbose { verboseLog }
120+
}
121+
}
122+
123+
private class TestSupplierLogger(override val thresholdLevel: LogLevel) : Logger {
124+
val errorLogs = mutableListOf<Pair<String, Throwable?>>()
125+
val warnLogs = mutableListOf<Pair<String, Throwable?>>()
126+
val infoLogs = mutableListOf<String>()
127+
val debugLogs = mutableListOf<String>()
128+
val verboseLogs = mutableListOf<String>()
129+
130+
override fun error(message: String) {
131+
errorLogs.add(Pair(message, null))
132+
}
133+
134+
override fun error(message: String, error: Throwable?) {
135+
errorLogs.add(Pair(message, error))
136+
}
137+
138+
override fun warn(message: String) {
139+
warnLogs.add(Pair(message, null))
140+
}
141+
142+
override fun warn(message: String, issue: Throwable?) {
143+
warnLogs.add(Pair(message, issue))
144+
}
145+
146+
override fun info(message: String) {
147+
infoLogs.add(message)
148+
}
149+
150+
override fun debug(message: String) {
151+
debugLogs.add(message)
152+
}
153+
154+
override fun verbose(message: String) {
155+
verboseLogs.add(message)
156+
}
157+
}

appsync/aws-appsync-events/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,6 @@ dependencies {
5353
testImplementation(libs.test.mockk)
5454
testImplementation(libs.test.kotlin.coroutines)
5555
testImplementation(libs.test.kotest.assertions)
56+
testImplementation(libs.test.kotest.assertions.json)
5657
testImplementation(libs.test.mockwebserver)
5758
}

appsync/aws-appsync-events/src/main/java/com/amplifyframework/aws/appsync/events/Events.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ import com.amplifyframework.aws.appsync.events.data.ChannelAuthorizers
2020
import com.amplifyframework.aws.appsync.events.data.EventsException
2121
import com.amplifyframework.aws.appsync.events.data.PublishResult
2222
import com.amplifyframework.aws.appsync.events.data.toEventsException
23-
import kotlinx.coroutines.coroutineScope
24-
import kotlinx.serialization.json.Json
23+
import com.amplifyframework.aws.appsync.events.utils.JsonUtils
2524
import kotlinx.serialization.json.JsonElement
2625
import okhttp3.OkHttpClient
2726

@@ -52,10 +51,7 @@ class Events(
5251
* @param defaultChannelAuthorizers passed to created channels if not overridden.
5352
*/
5453

55-
private val json = Json {
56-
encodeDefaults = true
57-
ignoreUnknownKeys = true
58-
}
54+
private val json = JsonUtils.createJsonForLibrary()
5955
private val endpoints = EventsEndpoints(endpoint)
6056
private val okHttpClient = OkHttpClient.Builder().apply {
6157
options.okHttpConfigurationProvider?.applyConfiguration(this)
@@ -121,15 +117,15 @@ class Events(
121117
fun channel(
122118
channelName: String,
123119
authorizers: ChannelAuthorizers = this.defaultChannelAuthorizers,
124-
) = EventsChannel(channelName, authorizers, eventsWebSocketProvider)
120+
) = EventsChannel(channelName, authorizers, eventsWebSocketProvider, json)
125121

126122
/**
127123
* Method to disconnect from all channels.
128124
*
129125
* @param flushEvents set to true (default) to allow all pending publish calls to succeed before disconnecting.
130126
* Setting to false will immediately disconnect, cancelling any in-progress or queued event publishes.
131127
*/
132-
suspend fun disconnect(flushEvents: Boolean = true): Unit = coroutineScope {
128+
suspend fun disconnect(flushEvents: Boolean = true) {
133129
eventsWebSocketProvider.existingWebSocket?.disconnect(flushEvents)
134130
}
135131
}

appsync/aws-appsync-events/src/main/java/com/amplifyframework/aws/appsync/events/EventsChannel.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import kotlinx.coroutines.flow.flow
3434
import kotlinx.coroutines.flow.flowOn
3535
import kotlinx.coroutines.flow.onCompletion
3636
import kotlinx.coroutines.flow.onStart
37+
import kotlinx.coroutines.withContext
38+
import kotlinx.serialization.json.Json
3739
import kotlinx.serialization.json.JsonArray
3840
import kotlinx.serialization.json.JsonElement
3941
import kotlinx.serialization.json.JsonPrimitive
@@ -47,7 +49,8 @@ import kotlinx.serialization.json.JsonPrimitive
4749
class EventsChannel internal constructor(
4850
val name: String,
4951
val authorizers: ChannelAuthorizers,
50-
private val eventsWebSocketProvider: EventsWebSocketProvider
52+
private val eventsWebSocketProvider: EventsWebSocketProvider,
53+
private val json: Json
5154
) {
5255

5356
/**
@@ -118,7 +121,7 @@ class EventsChannel internal constructor(
118121
private suspend fun publishToWebSocket(
119122
events: List<JsonElement>,
120123
authorizer: AppSyncAuthorizer
121-
): WebSocketMessage.Received.PublishSuccess = coroutineScope {
124+
): WebSocketMessage.Received.PublishSuccess = withContext(Dispatchers.IO) {
122125
val publishId = UUID.randomUUID().toString()
123126
val publishMessage = WebSocketMessage.Send.Publish(
124127
id = publishId,
@@ -134,7 +137,7 @@ class EventsChannel internal constructor(
134137
throw webSocket.disconnectReason?.toCloseException() ?: ConnectionClosedException()
135138
}
136139

137-
return@coroutineScope when (val response = deferredResponse.await()) {
140+
return@withContext when (val response = deferredResponse.await()) {
138141
is WebSocketMessage.Received.PublishSuccess -> {
139142
response
140143
}

0 commit comments

Comments
 (0)