Skip to content

Commit 2438647

Browse files
fix(datastore): Prevent datastore error with single auth rule when Mu… (#2760)
Co-authored-by: Tyler Roach <[email protected]>
1 parent 34d74b6 commit 2438647

File tree

2 files changed

+211
-2
lines changed

2 files changed

+211
-2
lines changed

aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthAppSyncGraphQLOperation.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,15 @@ public void onResponse(@NonNull Call call, @NonNull Response response) {
176176

177177
try {
178178
GraphQLResponse<R> graphQLResponse = wrapResponse(jsonResponse);
179-
if (graphQLResponse.hasErrors() && hasAuthRelatedErrors(graphQLResponse) && authTypes.hasNext()) {
180-
executorService.submit(MultiAuthAppSyncGraphQLOperation.this::dispatchRequest);
179+
if (graphQLResponse.hasErrors() && hasAuthRelatedErrors(graphQLResponse)) {
180+
if (authTypes.hasNext()) {
181+
executorService.submit(MultiAuthAppSyncGraphQLOperation.this::dispatchRequest);
182+
} else {
183+
onFailure.accept(new ApiAuthException(
184+
"Unable to successfully complete request with any of the compatible auth types.",
185+
"Check your application logs for detail."
186+
));
187+
}
181188
} else {
182189
onResponse.accept(graphQLResponse);
183190
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
2+
/*
3+
* Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License").
6+
* You may not use this file except in compliance with the License.
7+
* A copy of the License is located at
8+
*
9+
* http://aws.amazon.com/apache2.0
10+
*
11+
* or in the "license" file accompanying this file. This file is distributed
12+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
13+
* express or implied. See the License for the specific language governing
14+
* permissions and limitations under the License.
15+
*/
16+
17+
package com.amplifyframework.api.aws
18+
19+
import com.amplifyframework.api.ApiException
20+
import com.amplifyframework.api.ApiException.ApiAuthException
21+
import com.amplifyframework.api.aws.auth.ApiRequestDecoratorFactory
22+
import com.amplifyframework.api.aws.auth.RequestDecorator
23+
import com.amplifyframework.api.graphql.GraphQLResponse
24+
import com.amplifyframework.api.graphql.model.ModelQuery
25+
import com.amplifyframework.core.Consumer
26+
import com.amplifyframework.core.model.AuthStrategy
27+
import com.amplifyframework.core.model.Model
28+
import com.amplifyframework.core.model.ModelOperation
29+
import com.amplifyframework.core.model.annotations.AuthRule
30+
import com.amplifyframework.core.model.annotations.ModelConfig
31+
import io.mockk.every
32+
import io.mockk.mockk
33+
import io.mockk.verify
34+
import java.util.concurrent.ExecutorService
35+
import okhttp3.Callback
36+
import okhttp3.HttpUrl.Companion.toHttpUrl
37+
import okhttp3.OkHttpClient
38+
import okhttp3.Protocol
39+
import okhttp3.Request
40+
import okhttp3.Response
41+
import okhttp3.ResponseBody.Companion.toResponseBody
42+
import org.junit.Test
43+
import org.junit.runner.RunWith
44+
import org.robolectric.RobolectricTestRunner
45+
46+
@RunWith(RobolectricTestRunner::class)
47+
class MultiAuthAppSyncGraphQLOperationTest {
48+
49+
private val client = mockk<OkHttpClient>(relaxed = true)
50+
private val onResponse2 = mockk<Consumer<GraphQLResponse<ModelWithTwoAuthModes>>>(relaxed = true)
51+
private val onResponse1 = mockk<Consumer<GraphQLResponse<ModelWithOneAuthMode>>>(relaxed = true)
52+
private val onFailure = mockk<Consumer<ApiException>>(relaxed = true)
53+
private val executorService = mockk<ExecutorService>(relaxed = true)
54+
private val request2 = ModelQuery[ModelWithTwoAuthModes::class.java, "1"]
55+
private val request1 = ModelQuery[ModelWithOneAuthMode::class.java, "1"]
56+
private val responseFactoryMock = mockk<GsonGraphQLResponseFactory>()
57+
private val apiRequestDecoratorFactory = mockk<ApiRequestDecoratorFactory>()
58+
private val requestDecorator = mockk<RequestDecorator>()
59+
private val decoratedOkHttpRequest = mockk<Request>()
60+
private val mockCall = mockk<okhttp3.Call>()
61+
62+
@Test
63+
fun `submit dispatchRequest when more auth types available then fail`() {
64+
val operation = MultiAuthAppSyncGraphQLOperation.Builder<ModelWithTwoAuthModes>()
65+
.client(client)
66+
.request(request2)
67+
.responseFactory(responseFactoryMock)
68+
.onResponse(onResponse2)
69+
.onFailure(onFailure)
70+
.apiRequestDecoratorFactory(apiRequestDecoratorFactory)
71+
.executorService(executorService)
72+
.endpoint("https://amazon.com")
73+
.apiName("TestAPI")
74+
.build()
75+
76+
val response = buildResponse()
77+
val exception = buildAuthException()
78+
79+
val gqlErrors = buildGQLErrors()
80+
val gqlResponse = GraphQLResponse<ModelWithTwoAuthModes>(null, mutableListOf(gqlErrors))
81+
gqlResponse.errors.replaceAll { gqlErrors }
82+
every { responseFactoryMock.buildResponse<ModelWithTwoAuthModes>(any(), any(), any()) } returns gqlResponse
83+
84+
every { apiRequestDecoratorFactory.forAuthType(any()) } returns requestDecorator
85+
86+
every { requestDecorator.decorate(any()) } returns decoratedOkHttpRequest
87+
88+
every { executorService.submit(any()) } answers {
89+
firstArg<Runnable>().run()
90+
mockk()
91+
}
92+
// Mocks Callback
93+
every { client.newCall(decoratedOkHttpRequest) } returns mockCall
94+
every { mockCall.enqueue(any()) } answers {
95+
val callback = firstArg<Callback>()
96+
callback.onResponse(mockk(relaxed = true), response)
97+
}
98+
operation.start()
99+
100+
verify(exactly = 2) {
101+
executorService.submit(any())
102+
}
103+
verify {
104+
onFailure.accept(exception)
105+
}
106+
}
107+
108+
@Test
109+
fun `should invoke onFailure with single auth type and has auth error`() {
110+
val operation = MultiAuthAppSyncGraphQLOperation.Builder<ModelWithOneAuthMode>()
111+
.client(client)
112+
.request(request1)
113+
.responseFactory(responseFactoryMock)
114+
.onResponse(onResponse1)
115+
.onFailure(onFailure)
116+
.apiRequestDecoratorFactory(apiRequestDecoratorFactory)
117+
.executorService(executorService)
118+
.endpoint("https://amazon.com")
119+
.apiName("TestAPI")
120+
.build()
121+
122+
val response = buildResponse()
123+
val exception = buildAuthException()
124+
125+
val gqlErrors = buildGQLErrors()
126+
val gqlResponse = GraphQLResponse<ModelWithTwoAuthModes>(null, mutableListOf(gqlErrors))
127+
gqlResponse.errors.replaceAll { gqlErrors }
128+
129+
every { responseFactoryMock.buildResponse<ModelWithTwoAuthModes>(any(), any(), any()) } returns gqlResponse
130+
131+
every { apiRequestDecoratorFactory.forAuthType(any()) } returns requestDecorator
132+
133+
every { requestDecorator.decorate(any()) } returns decoratedOkHttpRequest
134+
135+
every { executorService.submit(any()) } answers {
136+
firstArg<Runnable>().run()
137+
mockk()
138+
}
139+
// Mocks Callback
140+
every { client.newCall(decoratedOkHttpRequest) } returns mockCall
141+
every { mockCall.enqueue(any()) } answers {
142+
val callback = firstArg<Callback>()
143+
callback.onResponse(mockk(relaxed = true), response)
144+
}
145+
146+
operation.start()
147+
148+
verify(exactly = 1) {
149+
executorService.submit(any())
150+
onFailure.accept(exception)
151+
}
152+
}
153+
154+
private fun buildResponse(): Response {
155+
val responseBody = "{\"errors\":" +
156+
" [{\"message\": \"Auth error\"," +
157+
" \"extensions\": {\"errorType\": \"Unauthorized\"}}]}"
158+
return Response.Builder()
159+
.code(200)
160+
.body(responseBody.toResponseBody())
161+
.request(Request(url = "https://amazon.com".toHttpUrl()))
162+
.protocol(Protocol.HTTP_1_0)
163+
.message("testing for submit dispatch request when more auth types available")
164+
.build()
165+
}
166+
167+
private fun buildAuthException(): ApiAuthException {
168+
return ApiAuthException(
169+
"Unable to successfully complete request with any of the compatible auth types.",
170+
"Check your application logs for detail."
171+
)
172+
}
173+
174+
private fun buildGQLErrors(): GraphQLResponse.Error {
175+
val extensions: MutableMap<String, Any?> = HashMap()
176+
extensions["errorType"] = "Unauthorized"
177+
return GraphQLResponse.Error("Unauthorized", null, null, extensions)
178+
}
179+
180+
@ModelConfig(
181+
authRules = [
182+
AuthRule(
183+
allow = AuthStrategy.OWNER,
184+
operations = [ModelOperation.CREATE, ModelOperation.UPDATE, ModelOperation.DELETE, ModelOperation.READ]
185+
), AuthRule(
186+
allow = AuthStrategy.PUBLIC,
187+
operations = [ModelOperation.CREATE, ModelOperation.UPDATE, ModelOperation.DELETE, ModelOperation.READ]
188+
)
189+
]
190+
)
191+
private class ModelWithTwoAuthModes : Model
192+
193+
@ModelConfig(
194+
authRules = [
195+
AuthRule(
196+
allow = AuthStrategy.OWNER,
197+
operations = [ModelOperation.CREATE, ModelOperation.UPDATE, ModelOperation.DELETE, ModelOperation.READ]
198+
)
199+
]
200+
)
201+
private class ModelWithOneAuthMode : Model
202+
}

0 commit comments

Comments
 (0)