Skip to content

Commit

Permalink
Merge branch 'main' into fix/datastore/nullable-custom-arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
gpanshu committed Aug 23, 2023
2 parents b929a99 + 07c0de3 commit 14f500b
Show file tree
Hide file tree
Showing 9 changed files with 452 additions and 22 deletions.
2 changes: 2 additions & 0 deletions aws-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ dependencies {
testImplementation(libs.test.jsonassert)
testImplementation(libs.test.junit)
testImplementation(libs.test.mockito.core)
testImplementation(libs.test.mockk)
testImplementation(libs.test.kotest.assertions)
testImplementation(libs.test.mockwebserver)
testImplementation(libs.rxjava)
testImplementation(libs.test.robolectric)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ private <R> GraphQLOperation<R> buildSubscriptionOperation(
if (AuthModeStrategyType.MULTIAUTH.equals(authModeStrategyType)) {
// If it gets here, we know that the request is an AppSyncGraphQLRequest because
// getAuthModeStrategyType checks for that, so we can safely cast the graphQLRequest.
return MutiAuthSubscriptionOperation.<R>builder()
return MultiAuthSubscriptionOperation.<R>builder()
.subscriptionEndpoint(clientDetails.getSubscriptionEndpoint())
.graphQlRequest((AppSyncGraphQLRequest<R>) graphQLRequest)
.responseFactory(gqlResponseFactory)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
package com.amplifyframework.api.aws;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import com.amplifyframework.AmplifyException;
import com.amplifyframework.api.ApiException;
import com.amplifyframework.api.ApiException.ApiAuthException;
import com.amplifyframework.api.aws.auth.AuthRuleRequestDecorator;
Expand All @@ -38,7 +38,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;

final class MutiAuthSubscriptionOperation<T> extends GraphQLOperation<T> {
final class MultiAuthSubscriptionOperation<T> extends GraphQLOperation<T> {
private static final Logger LOG = Amplify.Logging.logger(CategoryType.API, "amplify:aws-api");

private final SubscriptionEndpoint subscriptionEndpoint;
Expand All @@ -49,12 +49,11 @@ final class MutiAuthSubscriptionOperation<T> extends GraphQLOperation<T> {
private final Action onSubscriptionComplete;
private final AtomicBoolean canceled;
private final AuthRuleRequestDecorator requestDecorator;

private AuthorizationTypeIterator authTypes;
private String subscriptionId;
private Future<?> subscriptionFuture;

private MutiAuthSubscriptionOperation(Builder<T> builder) {
private MultiAuthSubscriptionOperation(Builder<T> builder) {
super(builder.graphQlRequest, builder.responseFactory);
this.subscriptionEndpoint = builder.subscriptionEndpoint;
this.onSubscriptionStart = builder.onSubscriptionStart;
Expand Down Expand Up @@ -115,12 +114,12 @@ private void dispatchRequest() {
request,
authorizationType,
subscriptionId -> {
MutiAuthSubscriptionOperation.this.subscriptionId = subscriptionId;
MultiAuthSubscriptionOperation.this.subscriptionId = subscriptionId;
onSubscriptionStart.accept(subscriptionId);
},
response -> {
if (response.hasErrors() && hasAuthRelatedErrors(response) && authTypes.hasNext()) {
// If there are auth-related errors, dispatch an ApiAuthException
// If there are auth-related errors queue up a retry with the next authType
executorService.submit(this::dispatchRequest);
} else {
// Otherwise, we just want to dispatch it as a next item and
Expand All @@ -139,8 +138,10 @@ private void dispatchRequest() {
onSubscriptionComplete
);
} else {
emitErrorAndCancelSubscription(new ApiException("Unable to establish subscription connection.",
AmplifyException.TODO_RECOVERY_SUGGESTION));
emitErrorAndCancelSubscription(new ApiAuthException(
"Unable to establish subscription connection with any of the compatible auth types.",
"Check your application logs for detail."
));
}

}
Expand Down Expand Up @@ -179,6 +180,21 @@ private void emitErrorAndCancelSubscription(ApiException apiException) {
onSubscriptionError.accept(apiException);
}

@VisibleForTesting
boolean isCanceled() {
return canceled.get();
}

@VisibleForTesting
void setCanceled(boolean canceled) {
this.canceled.set(canceled);
}

@VisibleForTesting
Future<?> getSubscriptionFuture() {
return subscriptionFuture;
}

static final class Builder<T> {
private SubscriptionEndpoint subscriptionEndpoint;
private AppSyncGraphQLRequest<T> graphQlRequest;
Expand Down Expand Up @@ -244,8 +260,8 @@ public Builder<T> requestDecorator(AuthRuleRequestDecorator requestDecorator) {
}

@NonNull
public MutiAuthSubscriptionOperation<T> build() {
return new MutiAuthSubscriptionOperation<>(this);
public MultiAuthSubscriptionOperation<T> build() {
return new MultiAuthSubscriptionOperation<>(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package com.amplifyframework.api.aws

import com.amplifyframework.api.ApiException
import com.amplifyframework.api.ApiException.ApiAuthException
import com.amplifyframework.api.aws.auth.AuthRuleRequestDecorator
import com.amplifyframework.api.graphql.GraphQLRequest
import com.amplifyframework.api.graphql.GraphQLResponse
import com.amplifyframework.core.Action
import com.amplifyframework.core.Consumer
import com.amplifyframework.core.model.AuthStrategy
import com.amplifyframework.core.model.Model
import com.amplifyframework.core.model.ModelOperation
import com.amplifyframework.core.model.ModelSchema
import com.amplifyframework.core.model.annotations.AuthRule
import com.amplifyframework.core.model.annotations.ModelConfig
import com.amplifyframework.util.MockExecutorService
import io.kotest.matchers.booleans.shouldBeTrue
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.shouldBe
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import java.util.concurrent.ExecutorService
import org.junit.Test

/**
* Unit tests for the [MultiAuthSubscriptionOperation] class.
*/
class MultiAuthSubscriptionOperationTest {

private val executor = MockExecutorService()
private val endpoint = MockSubscriptionEndpoint()
private val decorator: AuthRuleRequestDecorator = mockk(relaxed = true)

private val onStart: (String) -> Unit = mockk(relaxed = true)
private val onNext: (GraphQLResponse<String>) -> Unit = mockk(relaxed = true)
private val onError: (ApiException) -> Unit = mockk(relaxed = true)
private val onComplete: () -> Unit = mockk(relaxed = true)

//region start tests

@Test
fun `start emits exception if already cancelled`() {
val operation = createOperation()
operation.isCanceled = true
operation.start()
verify {
onError(any())
}
}

//endregion
//region cancellation tests

@Test
fun `cancel cancels the subscription future`() {
executor.autoRunTasks = false
val operation = createOperation()
operation.start()
operation.cancel()
operation.subscriptionFuture.isCancelled.shouldBeTrue()
}

@Test
fun `cancel does nothing if not started`() {
executor.autoRunTasks = false
val operation = createOperation()
operation.cancel()
executor.queue.shouldBeEmpty()
}

@Test
fun `cancel releases subscription`() {
val operation = createOperation()
operation.start()
endpoint.invokeOnStart()
operation.cancel()
endpoint.verifyReleased()
}

//endregion
//region error tests

@Test
fun `operation is cancelled if authtypes are exhausted`() {
val operation = createOperation()
operation.start()
endpoint.invokeOnStart()
endpoint.invokeOnError(ApiAuthException("", "")) // error on first auth type
endpoint.invokeOnStart()
endpoint.invokeOnError(ApiAuthException("", "")) // error on second auth type
endpoint.verifyReleased()
}

@Test
fun `operation emits ApiAuthException if authtypes are exhausted`() {
val operation = createOperation()
operation.start()
endpoint.invokeOnError(ApiAuthException("", "")) // error on first auth type
endpoint.invokeOnError(ApiAuthException("", "")) // error on second auth type
verify {
onError(any<ApiAuthException>())
}
}

@Test
fun `next authtype is tried if requestDecorator throws ApiAuthException`() {
every { decorator.decorate(any<GraphQLRequest<*>>(), any()) } throws ApiAuthException("", "")
val operation = createOperation()
operation.start()
executor.numTasksQueued shouldBe 2 // One for each auth type
endpoint.verifyNumSubscriptionRequests(1) // Only one should send a subscription request
}

@Test
fun `next authtype is tried if response contains auth errors`() {
val response = mockAuthErrorResponse()
val operation = createOperation()
operation.start()
endpoint.invokeOnResponse(response)
executor.numTasksQueued shouldBe 2 // One for each auth type
endpoint.verifyNumSubscriptionRequests(2)
}

@Test
fun `error is emitted if requestDecorator throws ApiException`() {
val exception = ApiException("", "")
every { decorator.decorate(any<GraphQLRequest<*>>(), any()) } throws exception
val operation = createOperation()
operation.start()
verify {
onError(exception)
}
}

//endregion
//region Success tests

@Test
fun `onStart is called`() {
val operation = createOperation()
operation.start()
endpoint.invokeOnStart()
verify {
onStart(endpoint.subscriptionId)
}
}

@Test
fun `onNext is called`() {
val response = mockk<GraphQLResponse<String>>() {
every { hasErrors() } returns false
}
val operation = createOperation()
operation.start()
endpoint.invokeOnResponse(response)
verify {
onNext(response)
}
}

@Test
fun `onComplete is called`() {
val operation = createOperation()
operation.start()
endpoint.invokeOnComplete()
verify {
onComplete()
}
}

//endregion

private fun createOperation(
subscriptionEndpoint: SubscriptionEndpoint = endpoint.endpoint,
onSubscriptionStart: (String) -> Unit = onStart,
onNextItem: (GraphQLResponse<String>) -> Unit = onNext,
onSubscriptionError: (ApiException) -> Unit = onError,
onSubscriptionComplete: () -> Unit = onComplete,
executorService: ExecutorService = executor,
requestDecorator: AuthRuleRequestDecorator = decorator,
graphQlRequest: AppSyncGraphQLRequest<String> = mockk {
every { modelSchema } returns ModelSchema.fromModelClass(ModelWithTwoAuthModes::class.java)
every { authRuleOperation } returns ModelOperation.READ
every { content } returns ""
}
): MultiAuthSubscriptionOperation<String> {
val operation = MultiAuthSubscriptionOperation.builder<String>()
.subscriptionEndpoint(subscriptionEndpoint)
.onSubscriptionStart(onSubscriptionStart)
.onNextItem(onNextItem)
.onSubscriptionError(onSubscriptionError)
.onSubscriptionComplete(onSubscriptionComplete)
.executorService(executorService)
.requestDecorator(requestDecorator)
.graphQlRequest(graphQlRequest)
.build()
return operation
}

private fun mockAuthErrorResponse() = mockk<GraphQLResponse<String>> {
every { hasErrors() } returns true
every { errors } returns listOf(
mockk { every { extensions } returns mapOf("errorType" to "Unauthorized") }
)
}

private class MockSubscriptionEndpoint {
val endpoint = mockk<SubscriptionEndpoint>() {
every { releaseSubscription(any()) } just Runs
}
val subscriptionId = "subId"

var onStart: Consumer<String>? = null
var onResponse: Consumer<GraphQLResponse<String>>? = null
var onError: Consumer<ApiException>? = null
var onComplete: Action? = null

init {
every { endpoint.requestSubscription<String>(any(), any(), any(), any(), any(), any()) } answers {
onStart = arg(2)
onResponse = arg(3)
onError = arg(4)
onComplete = arg(5)
}
}

fun invokeOnStart() = onStart?.accept(subscriptionId)
fun invokeOnResponse(response: GraphQLResponse<String>) = onResponse?.accept(response)
fun invokeOnError(error: ApiException) = onError?.accept(error)
fun invokeOnComplete() = onComplete?.call()

fun verifyReleased() = verify {
endpoint.releaseSubscription(subscriptionId)
}

fun verifyNumSubscriptionRequests(expected: Int) = verify(exactly = expected) {
endpoint.requestSubscription<String>(any(), any(), any(), any(), any(), any())
}
}

@ModelConfig(
authRules = [
AuthRule(
allow = AuthStrategy.OWNER,
operations = [ModelOperation.CREATE, ModelOperation.UPDATE, ModelOperation.DELETE, ModelOperation.READ]
), AuthRule(
allow = AuthStrategy.PUBLIC,
operations = [ModelOperation.CREATE, ModelOperation.UPDATE, ModelOperation.DELETE, ModelOperation.READ]
)
]
)
private class ModelWithTwoAuthModes : Model
}
Loading

0 comments on commit 14f500b

Please sign in to comment.