diff --git a/.github/workflows/maven_release_publisher.yml b/.github/workflows/maven_release_publisher.yml
index 7b695912ad..4da0b190cd 100644
--- a/.github/workflows/maven_release_publisher.yml
+++ b/.github/workflows/maven_release_publisher.yml
@@ -20,9 +20,11 @@ jobs:
with:
project-name: AmplifyAndroid-IntegrationTest
env-vars-for-codebuild: |
- ORG_GRADLE_PROJECT_useAwsSdkReleaseBuild
+ ORG_GRADLE_PROJECT_useAwsSdkReleaseBuild,
+ NUMBER_OF_DEVICES_TO_TEST
env:
ORG_GRADLE_PROJECT_useAwsSdkReleaseBuild: true
+ NUMBER_OF_DEVICES_TO_TEST: 3
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index c143f7d6fe..42cc79e213 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,6 +1,12 @@
+
+
+
+
-
\ No newline at end of file
+
diff --git a/README.md b/README.md
index f33c7255ca..e6cef8f068 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ Guide](https://docs.amplify.aws/start/q/integration/android).
| Category | AWS Provider | Description |
|-------------------------------------------------------------------------------------------------|--------------|--------------------------------------------|
-| **[Authentication](https://docs.amplify.aws/lib/devpreview/getting-started/q/platform/android/)** | Cognito | Building blocks to create auth experiences
*Note: Authentication category only supports **Sign Up**, **Sign In**, **Sign Out**, **Fetch Auth Session** and **getCurrentUser** API's.* |
+| **[Authentication](https://docs.amplify.aws/lib/devpreview/getting-started/q/platform/android/)** | Cognito | Building blocks to create auth experiences |
| **[Storage](https://docs.amplify.aws/lib/storage/getting-started/q/platform/android)** | S3 | Manages content in public, protected, private storage buckets |
| **[DataStore](https://docs.amplify.aws/lib/datastore/getting-started/q/platform/android)** | AppSync | Programming model for shared and distributed data, with simple online/offline synchronization |
| **[API (GraphQL)](https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/android)** | AppSync | Interact with your GraphQL or AppSync endpoint |
diff --git a/aws-analytics-pinpoint/src/androidTest/java/com/amplifyframework/analytics/pinpoint/PinpointAnalyticsStressTest.kt b/aws-analytics-pinpoint/src/androidTest/java/com/amplifyframework/analytics/pinpoint/PinpointAnalyticsStressTest.kt
new file mode 100644
index 0000000000..4233591695
--- /dev/null
+++ b/aws-analytics-pinpoint/src/androidTest/java/com/amplifyframework/analytics/pinpoint/PinpointAnalyticsStressTest.kt
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2022 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.analytics.pinpoint
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.util.Log
+import android.util.Pair
+import androidx.annotation.RawRes
+import androidx.test.core.app.ApplicationProvider
+import aws.sdk.kotlin.services.pinpoint.PinpointClient
+import aws.sdk.kotlin.services.pinpoint.model.EndpointLocation
+import aws.sdk.kotlin.services.pinpoint.model.EndpointResponse
+import aws.sdk.kotlin.services.pinpoint.model.GetEndpointRequest
+import com.amplifyframework.analytics.AnalyticsEvent
+import com.amplifyframework.analytics.AnalyticsProperties
+import com.amplifyframework.analytics.UserProfile
+import com.amplifyframework.analytics.pinpoint.models.AWSPinpointUserProfile
+import com.amplifyframework.auth.AuthPlugin
+import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin
+import com.amplifyframework.core.Amplify
+import com.amplifyframework.hub.HubChannel
+import com.amplifyframework.hub.HubEvent
+import com.amplifyframework.testutils.HubAccumulator
+import com.amplifyframework.testutils.Resources
+import com.amplifyframework.testutils.Sleep
+import com.amplifyframework.testutils.sync.SynchronousAuth
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.runBlocking
+import org.json.JSONException
+import org.junit.Assert
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+
+class PinpointAnalyticsStressTest {
+
+ companion object {
+ private const val CREDENTIALS_RESOURCE_NAME = "credentials"
+ private const val CONFIGURATION_NAME = "amplifyconfiguration"
+ private const val COGNITO_CONFIGURATION_TIMEOUT = 5 * 1000L
+ private const val PINPOINT_ROUNDTRIP_TIMEOUT = 1 * 1000L
+ private const val FLUSH_TIMEOUT = 1 * 500L
+ private const val RECORD_INSERTION_TIMEOUT = 1 * 1000L
+ private const val UNIQUE_ID_KEY = "UniqueId"
+ private const val PREFERENCES_AND_FILE_MANAGER_SUFFIX = "515d6767-01b7-49e5-8273-c8d11b0f331d"
+ private lateinit var synchronousAuth: SynchronousAuth
+ private lateinit var preferences: SharedPreferences
+ private lateinit var appId: String
+ private lateinit var uniqueId: String
+ private lateinit var pinpointClient: PinpointClient
+
+ @BeforeClass
+ @JvmStatic
+ fun setupBefore() {
+ val context = ApplicationProvider.getApplicationContext()
+ @RawRes val resourceId = Resources.getRawResourceId(context, CONFIGURATION_NAME)
+ appId = readAppIdFromResource(context, resourceId)
+ preferences = context.getSharedPreferences(
+ "${appId}$PREFERENCES_AND_FILE_MANAGER_SUFFIX",
+ Context.MODE_PRIVATE
+ )
+ setUniqueId()
+ Amplify.Auth.addPlugin(AWSCognitoAuthPlugin() as AuthPlugin<*>)
+ Amplify.addPlugin(AWSPinpointAnalyticsPlugin())
+ Amplify.configure(context)
+ Sleep.milliseconds(COGNITO_CONFIGURATION_TIMEOUT)
+ synchronousAuth = SynchronousAuth.delegatingTo(Amplify.Auth)
+ }
+
+ private fun setUniqueId() {
+ uniqueId = UUID.randomUUID().toString()
+ preferences.edit().putString(UNIQUE_ID_KEY, uniqueId).commit()
+ }
+
+ private fun readCredentialsFromResource(context: Context, @RawRes resourceId: Int): Pair? {
+ val resource = Resources.readAsJson(context, resourceId)
+ var userCredentials: Pair? = null
+ return try {
+ val credentials = resource.getJSONArray("credentials")
+ for (index in 0 until credentials.length()) {
+ val credential = credentials.getJSONObject(index)
+ val username = credential.getString("username")
+ val password = credential.getString("password")
+ userCredentials = Pair(username, password)
+ }
+ userCredentials
+ } catch (jsonReadingFailure: JSONException) {
+ throw RuntimeException(jsonReadingFailure)
+ }
+ }
+
+ private fun readAppIdFromResource(context: Context, @RawRes resourceId: Int): String {
+ val resource = Resources.readAsJson(context, resourceId)
+ return try {
+ val analyticsJson = resource.getJSONObject("analytics")
+ val pluginsJson = analyticsJson.getJSONObject("plugins")
+ val pluginJson = pluginsJson.getJSONObject("awsPinpointAnalyticsPlugin")
+ val pinpointJson = pluginJson.getJSONObject("pinpointAnalytics")
+ pinpointJson.getString("appId")
+ } catch (jsonReadingFailure: JSONException) {
+ throw RuntimeException(jsonReadingFailure)
+ }
+ }
+ }
+
+ @Before
+ fun flushEvents() {
+ val context = ApplicationProvider.getApplicationContext()
+ @RawRes val resourceId = Resources.getRawResourceId(context, CREDENTIALS_RESOURCE_NAME)
+ val userAndPasswordPair = readCredentialsFromResource(context, resourceId)
+ synchronousAuth.signOut()
+ synchronousAuth.signIn(
+ userAndPasswordPair!!.first,
+ userAndPasswordPair.second
+ )
+ val hubAccumulator =
+ HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 1).start()
+ Amplify.Analytics.flushEvents()
+ hubAccumulator.await(10, TimeUnit.SECONDS)
+ pinpointClient = Amplify.Analytics.getPlugin("awsPinpointAnalyticsPlugin").escapeHatch as
+ PinpointClient
+ uniqueId = preferences.getString(UNIQUE_ID_KEY, "error-no-unique-id")!!
+ Assert.assertNotEquals(uniqueId, "error-no-unique-id")
+ }
+
+ /**
+ * Calls Analytics.recordEvent on an event with 5 attributes 50 times
+ */
+ @Test
+ fun testMultipleRecordEvent() {
+ var eventName: String
+ val hubAccumulator =
+ HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 2).start()
+
+ repeat(50) {
+ eventName = "Amplify-event" + UUID.randomUUID().toString()
+ val event = AnalyticsEvent.builder()
+ .name(eventName)
+ .addProperty("AnalyticsStringProperty", "Pancakes")
+ .addProperty("AnalyticsBooleanProperty", true)
+ .addProperty("AnalyticsDoubleProperty", 3.14)
+ .addProperty("AnalyticsIntegerProperty", 42)
+ .build()
+
+ Amplify.Analytics.recordEvent(event)
+ }
+
+ Amplify.Analytics.flushEvents()
+ val hubEvents = hubAccumulator.await(10, TimeUnit.SECONDS)
+ val submittedEvents = combineAndFilterEvents(hubEvents)
+ Assert.assertEquals(50, submittedEvents.size.toLong())
+ }
+
+ /**
+ * Calls Analytics.recordEvent on an event with 40 attributes 50 times
+ */
+ @Test
+ fun testLargeMultipleRecordEvent() {
+ var eventName: String
+ val hubAccumulator =
+ HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 2).start()
+
+ repeat(50) {
+ eventName = "Amplify-event" + UUID.randomUUID().toString()
+ val event = AnalyticsEvent.builder()
+ event.name(eventName)
+ for (i in 1..50) {
+ event.addProperty("AnalyticsStringProperty$i", "Pancakes")
+ }
+
+ Amplify.Analytics.recordEvent(event.build())
+ }
+
+ Amplify.Analytics.flushEvents()
+ val hubEvents = hubAccumulator.await(10, TimeUnit.SECONDS)
+ val submittedEvents = combineAndFilterEvents(hubEvents)
+ Assert.assertEquals(50, submittedEvents.size.toLong())
+ }
+
+ /**
+ * Calls Analytics.flushEvent 50 times
+ */
+ @Test
+ fun testMultipleFlushEvent() {
+ val analyticsHubEventAccumulator =
+ HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 50)
+ .start()
+ val eventName = "Amplify-event" + UUID.randomUUID().toString()
+ val event = AnalyticsEvent.builder()
+ .name(eventName)
+ .addProperty("AnalyticsStringProperty", "Pancakes")
+ .build()
+ Amplify.Analytics.recordEvent(event)
+
+ repeat(50) {
+ Amplify.Analytics.flushEvents()
+ Sleep.milliseconds(FLUSH_TIMEOUT)
+ }
+
+ val hubEvents = analyticsHubEventAccumulator.await(10, TimeUnit.SECONDS)
+ val submittedEvents = combineAndFilterEvents(hubEvents)
+ Assert.assertEquals(1, submittedEvents.size.toLong())
+ Assert.assertEquals(eventName, submittedEvents[0].name)
+ }
+
+ /**
+ * calls Analytics.recordEvent, then calls Analytics.flushEvent; 30 times
+ */
+ @Test
+ fun testFlushEvent_AfterRecordEvent() {
+ var eventName: String
+ val analyticsHubEventAccumulator =
+ HubAccumulator.create(HubChannel.ANALYTICS, AnalyticsChannelEventName.FLUSH_EVENTS, 35)
+ .start()
+
+ repeat(30) {
+ eventName = "Amplify-event" + UUID.randomUUID().toString()
+ val event = AnalyticsEvent.builder()
+ .name(eventName)
+ .addProperty("AnalyticsStringProperty", "Pancakes")
+ .build()
+ Amplify.Analytics.recordEvent(event)
+ Sleep.milliseconds(RECORD_INSERTION_TIMEOUT)
+ Amplify.Analytics.flushEvents()
+ Sleep.milliseconds(FLUSH_TIMEOUT)
+ }
+ val hubEvents = analyticsHubEventAccumulator.await(30, TimeUnit.SECONDS)
+ val submittedEvents = combineAndFilterEvents(hubEvents)
+ Assert.assertEquals(30, submittedEvents.size.toLong())
+ }
+
+ /**
+ * Calls Analytics.identifyUser on a user with few attributes 20 times
+ */
+ @Test
+ fun testMultipleIdentifyUser() {
+ val location = testLocation
+ val properties = endpointProperties
+ val userProfile = AWSPinpointUserProfile.builder()
+ .name("test-user")
+ .email("user@test.com")
+ .plan("test-plan")
+ .location(location)
+ .customProperties(properties)
+ .build()
+ repeat(20) {
+ Amplify.Analytics.identifyUser(UUID.randomUUID().toString(), userProfile)
+ Sleep.milliseconds(PINPOINT_ROUNDTRIP_TIMEOUT)
+ val endpointResponse = fetchEndpointResponse()
+ assertCommonEndpointResponseProperties(endpointResponse)
+ }
+ }
+
+ /**
+ * Calls Analytics.identifyUser on a user with 100+ attributes 20 times
+ */
+ @Test
+ fun testLargeMultipleIdentifyUser() {
+ val location = testLocation
+ val properties = endpointProperties
+ val userAttributes = largeUserAttributes
+ val pinpointUserProfile = AWSPinpointUserProfile.builder()
+ .name("test-user")
+ .email("user@test.com")
+ .plan("test-plan")
+ .location(location)
+ .customProperties(properties)
+ .userAttributes(userAttributes)
+ .build()
+ repeat(20) {
+ Amplify.Analytics.identifyUser(UUID.randomUUID().toString(), pinpointUserProfile)
+ Sleep.milliseconds(PINPOINT_ROUNDTRIP_TIMEOUT)
+ val endpointResponse = fetchEndpointResponse()
+ assertCommonEndpointResponseProperties(endpointResponse)
+ }
+ }
+
+ private fun fetchEndpointResponse(): EndpointResponse {
+ var endpointResponse: EndpointResponse? = null
+ runBlocking {
+ endpointResponse = pinpointClient.getEndpoint(
+ GetEndpointRequest.invoke {
+ this.applicationId = appId
+ this.endpointId = uniqueId
+ }
+ ).endpointResponse
+ }
+ assert(null != endpointResponse)
+ return endpointResponse!!
+ }
+
+ private fun assertCommonEndpointResponseProperties(endpointResponse: EndpointResponse) {
+ Log.i("DEBUG", endpointResponse.toString())
+ val attributes = endpointResponse.attributes!!
+ Assert.assertEquals("user@test.com", attributes["email"]!![0])
+ Assert.assertEquals("test-user", attributes["name"]!![0])
+ Assert.assertEquals("test-plan", attributes["plan"]!![0])
+ val endpointProfileLocation: EndpointLocation = endpointResponse.location!!
+ Assert.assertEquals(47.6154086, endpointProfileLocation.latitude, 0.1)
+ Assert.assertEquals((-122.3349685), endpointProfileLocation.longitude, 0.1)
+ Assert.assertEquals("98122", endpointProfileLocation.postalCode)
+ Assert.assertEquals("Seattle", endpointProfileLocation.city)
+ Assert.assertEquals("WA", endpointProfileLocation.region)
+ Assert.assertEquals("USA", endpointProfileLocation.country)
+ Assert.assertEquals("TestStringValue", attributes["TestStringProperty"]!![0])
+ Assert.assertEquals(1.0, endpointResponse.metrics!!["TestDoubleProperty"]!!, 0.1)
+ }
+
+ private val largeUserAttributes: AnalyticsProperties
+ get() {
+ val analyticsProperties = AnalyticsProperties.builder()
+ for (i in 1..100) {
+ analyticsProperties.add("SomeUserAttribute$i", "User attribute value")
+ }
+ return analyticsProperties.build()
+ }
+
+ private val endpointProperties: AnalyticsProperties
+ get() {
+ return AnalyticsProperties.builder()
+ .add("TestStringProperty", "TestStringValue")
+ .add("TestDoubleProperty", 1.0)
+ .build()
+ }
+ private val testLocation: UserProfile.Location
+ get() {
+ return UserProfile.Location.builder()
+ .latitude(47.6154086)
+ .longitude(-122.3349685)
+ .postalCode("98122")
+ .city("Seattle")
+ .region("WA")
+ .country("USA")
+ .build()
+ }
+
+ private fun combineAndFilterEvents(hubEvents: List>): MutableList {
+ val result = mutableListOf()
+ hubEvents.forEach {
+ if ((it.data as List<*>).isNotEmpty()) {
+ (it.data as ArrayList<*>).forEach { event ->
+ if (!(event as AnalyticsEvent).name.startsWith("_session")) {
+ result.add(event)
+ }
+ }
+ }
+ }
+ return result
+ }
+}
diff --git a/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/ModelWithMetadataAdapterTest.java b/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/ModelWithMetadataAdapterTest.java
index de7683b098..e92f20d187 100644
--- a/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/ModelWithMetadataAdapterTest.java
+++ b/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/ModelWithMetadataAdapterTest.java
@@ -140,7 +140,7 @@ public void adapterCanDeserializeJsonOfSerializedModelIntoMwm() throws AmplifyEx
String json = Resources.readAsString("serialized-model-with-metadata.json");
Type type = TypeMaker.getParameterizedType(ModelWithMetadata.class, SerializedModel.class);
ModelWithMetadata actual = gson.fromJson(json, type);
-
+
// Assert that the deserialized output matches out expected value
Assert.assertEquals(expected, actual);
}
diff --git a/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AuthStressTests.kt b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AuthStressTests.kt
index 1ec3fac4d7..f57d4861b6 100644
--- a/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AuthStressTests.kt
+++ b/aws-auth-cognito/src/androidTest/java/com/amplifyframework/auth/cognito/AuthStressTests.kt
@@ -19,6 +19,8 @@ import android.content.Context
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import com.amplifyframework.AmplifyException
+import com.amplifyframework.auth.AuthUserAttribute
+import com.amplifyframework.auth.AuthUserAttributeKey
import com.amplifyframework.auth.cognito.result.AWSCognitoAuthSignOutResult
import com.amplifyframework.auth.cognito.testutils.Credentials
import com.amplifyframework.auth.options.AuthFetchSessionOptions
@@ -34,6 +36,14 @@ import org.junit.Test
class AuthStressTests {
companion object {
private const val TIMEOUT_S = 20L
+ val attributes = listOf(
+ (AuthUserAttribute(AuthUserAttributeKey.address(), "Sesame Street")),
+ (AuthUserAttribute(AuthUserAttributeKey.name(), "Elmo")),
+ (AuthUserAttribute(AuthUserAttributeKey.gender(), "Male")),
+ (AuthUserAttribute(AuthUserAttributeKey.birthdate(), "February 3")),
+ (AuthUserAttribute(AuthUserAttributeKey.phoneNumber(), "+16268319333")),
+ (AuthUserAttribute(AuthUserAttributeKey.updatedAt(), "${System.currentTimeMillis()}"))
+ )
@BeforeClass
@JvmStatic
@@ -66,6 +76,9 @@ class AuthStressTests {
}
}
+ /**
+ * Calls Auth.signIn 50 times
+ */
@Test
fun testMultipleSignIn() {
val successLatch = CountDownLatch(1)
@@ -84,6 +97,9 @@ class AuthStressTests {
assertTrue(errorLatch.await(TIMEOUT_S, TimeUnit.SECONDS))
}
+ /**
+ * Calls Auth.signOut 50 times
+ */
@Test
fun testMultipleSignOut() {
val latch = CountDownLatch(50)
@@ -97,6 +113,9 @@ class AuthStressTests {
assertTrue(latch.await(TIMEOUT_S, TimeUnit.SECONDS))
}
+ /**
+ * Calls Auth.fetchAuthSession 100 times when signed out
+ */
@Test
fun testMultipleFAS_WhenSignedOut() {
val latch = CountDownLatch(100)
@@ -108,6 +127,9 @@ class AuthStressTests {
assertTrue(latch.await(TIMEOUT_S, TimeUnit.SECONDS))
}
+ /**
+ * Calls Auth.signIn, then calls Auth.fetchAuthSession 100 times
+ */
@Test
fun testMultipleFAS_AfterSignIn() {
val latch = CountDownLatch(101)
@@ -126,6 +148,9 @@ class AuthStressTests {
assertTrue(latch.await(TIMEOUT_S, TimeUnit.SECONDS))
}
+ /**
+ * Calls Auth.signIn, then calls Auth.signOut
+ */
@Test
fun testSignOut_AfterSignIn() {
val latch = CountDownLatch(2)
@@ -142,6 +167,9 @@ class AuthStressTests {
assertTrue(latch.await(TIMEOUT_S, TimeUnit.SECONDS))
}
+ /**
+ * Calls Auth.signIn, calls Auth.fetchAuthSession 100 times, then calls Auth.signOut
+ */
@Test
fun testSignIn_multipleFAS_SignOut() {
val latch = CountDownLatch(102)
@@ -162,6 +190,9 @@ class AuthStressTests {
assertTrue(latch.await(TIMEOUT_S, TimeUnit.SECONDS))
}
+ /**
+ * Calls Auth.signIn, then calls Auth.fetchAuthSession 100 times. Randomly calls Auth.signOut within those 100 calls.
+ */
@Test
fun testSignIn_multipleFAS_withSignOut() {
val latch = CountDownLatch(102)
@@ -187,6 +218,10 @@ class AuthStressTests {
assertTrue(latch.await(TIMEOUT_S, TimeUnit.SECONDS))
}
+ /**
+ * Calls Auth.signIn, then calls Auth.fetchAuthSession 100 times. Randomly calls Auth.fetchAuthSession with
+ * forceRefresh() within those 100 calls.
+ */
@Test
fun testSignIn_multipleFAS_withRefresh() {
val latch = CountDownLatch(101)
@@ -212,6 +247,9 @@ class AuthStressTests {
assertTrue(latch.await(TIMEOUT_S, TimeUnit.MINUTES))
}
+ /**
+ * Randomly calls Auth.fetchAuthSession, Auth.signIn, Auth.fetchAuthSession with forceRefresh(), and Auth.signOut 20 times.
+ */
@Test
fun testRandomMultipleAPIs() {
val latch = CountDownLatch(20)
@@ -240,4 +278,74 @@ class AuthStressTests {
assertTrue(latch.await(TIMEOUT_S, TimeUnit.MINUTES))
}
+
+ /**
+ * Calls Auth.getCurrentUser 100 times
+ */
+ @Test
+ fun testSignIn_GetCurrentUser() {
+ val latch = CountDownLatch(101)
+ Amplify.Auth.signIn(
+ username,
+ password,
+ { if (it.isSignedIn) latch.countDown() else fail() },
+ { fail() }
+ )
+
+ repeat(100) {
+ Amplify.Auth.getCurrentUser(
+ { if (it.username == username) latch.countDown() else fail() },
+ { fail() }
+ )
+ }
+
+ assertTrue(latch.await(TIMEOUT_S, TimeUnit.MINUTES))
+ }
+
+ /**
+ * Calls Auth.fetchUserAttributes 100 times
+ */
+ @Test
+ fun testSignIn_FetchAttributes() {
+ val latch = CountDownLatch(101)
+ Amplify.Auth.signIn(
+ username,
+ password,
+ { if (it.isSignedIn) latch.countDown() else fail() },
+ { fail() }
+ )
+
+ repeat(100) {
+ Amplify.Auth.fetchUserAttributes(
+ { latch.countDown() },
+ { fail() }
+ )
+ }
+
+ assertTrue(latch.await(TIMEOUT_S, TimeUnit.MINUTES))
+ }
+
+ /**
+ * Calls Auth.updateUserAttributes 100 times
+ */
+ @Test
+ fun testSignIn_UpdateAttributes() {
+ val latch = CountDownLatch(101)
+ Amplify.Auth.signIn(
+ username,
+ password,
+ { if (it.isSignedIn) latch.countDown() else fail() },
+ { fail() }
+ )
+
+ repeat(100) {
+ Amplify.Auth.updateUserAttributes(
+ attributes,
+ { latch.countDown() },
+ { fail() }
+ )
+ }
+
+ assertTrue(latch.await(TIMEOUT_S, TimeUnit.MINUTES))
+ }
}
diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt
index f977ad5fd0..81350e123a 100644
--- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt
+++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt
@@ -263,21 +263,33 @@ internal class RealAWSCognitoAuthPlugin(
)
}
- val authSignUpResult = AuthSignUpResult(
- false,
- AuthNextSignUpStep(
- AuthSignUpStep.CONFIRM_SIGN_UP_STEP,
- mapOf(),
- AuthCodeDeliveryDetails(
- deliveryDetails?.getValue("DESTINATION") ?: "",
- AuthCodeDeliveryDetails.DeliveryMedium.fromString(
- deliveryDetails?.getValue("MEDIUM")
- ),
- deliveryDetails?.getValue("ATTRIBUTE")
- )
- ),
- response?.userSub
- )
+ val authSignUpResult = if (response?.userConfirmed == true) {
+ AuthSignUpResult(
+ true,
+ AuthNextSignUpStep(
+ AuthSignUpStep.DONE,
+ mapOf(),
+ null
+ ),
+ response.userSub
+ )
+ } else {
+ AuthSignUpResult(
+ false,
+ AuthNextSignUpStep(
+ AuthSignUpStep.CONFIRM_SIGN_UP_STEP,
+ mapOf(),
+ AuthCodeDeliveryDetails(
+ deliveryDetails?.getValue("DESTINATION") ?: "",
+ AuthCodeDeliveryDetails.DeliveryMedium.fromString(
+ deliveryDetails?.getValue("MEDIUM")
+ ),
+ deliveryDetails?.getValue("ATTRIBUTE")
+ )
+ ),
+ response?.userSub
+ )
+ }
onSuccess.accept(authSignUpResult)
logger.verbose("SignUp Execution complete")
} catch (exception: Exception) {
diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/CredentialStoreState.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/CredentialStoreState.kt
index 2e898502eb..aa8f01647f 100644
--- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/CredentialStoreState.kt
+++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/states/CredentialStoreState.kt
@@ -64,7 +64,10 @@ internal sealed class CredentialStoreState : State {
val action = credentialStoreActions.loadCredentialStoreAction(storeEvent.credentialType)
StateResolution(LoadingStoredCredentials(), listOf(action))
}
- is CredentialStoreEvent.EventType.ThrowError -> StateResolution(Error(storeEvent.error))
+ is CredentialStoreEvent.EventType.ThrowError -> {
+ val action = credentialStoreActions.moveToIdleStateAction()
+ StateResolution(Error(storeEvent.error), listOf(action))
+ }
else -> defaultResolution
}
is LoadingStoredCredentials, is StoringCredentials, is ClearingCredentials -> when (storeEvent) {
@@ -96,8 +99,7 @@ internal sealed class CredentialStoreState : State {
}
is Success, is Error -> when (storeEvent) {
is CredentialStoreEvent.EventType.MoveToIdleState -> {
- val action = credentialStoreActions.moveToIdleStateAction()
- StateResolution(Idle(), listOf(action))
+ StateResolution(Idle(), listOf())
}
else -> StateResolution(oldState)
}
diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/JsonGenerator.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/JsonGenerator.kt
index b91a8981bf..837dd9116e 100644
--- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/JsonGenerator.kt
+++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/JsonGenerator.kt
@@ -61,6 +61,6 @@ object JsonGenerator {
}
fun main() {
- cleanDirectory()
+ // cleanDirectory()
JsonGenerator.generate()
}
diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/SerializationTools.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/SerializationTools.kt
index 4484cd8695..0b2c34d7fb 100644
--- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/SerializationTools.kt
+++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/SerializationTools.kt
@@ -24,6 +24,7 @@ import com.amplifyframework.auth.cognito.featuretest.serializers.CognitoIdentity
import com.amplifyframework.auth.cognito.featuretest.serializers.CognitoIdentityProviderExceptionSerializer
import com.amplifyframework.auth.cognito.featuretest.serializers.deserializeToAuthState
import com.amplifyframework.auth.cognito.featuretest.serializers.serialize
+import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions
import com.amplifyframework.auth.result.AuthSessionResult
import com.amplifyframework.statemachine.codegen.states.AuthState
import com.google.gson.Gson
@@ -56,7 +57,7 @@ fun writeFile(json: String, dirName: String, fileName: String) {
}
fun cleanDirectory() {
- val directory = File("$basePath")
+ val directory = File(basePath)
if (directory.exists()) {
directory.deleteRecursively()
}
@@ -173,6 +174,7 @@ fun Any?.toJsonElement(): JsonElement {
is String -> JsonPrimitive(this)
is Instant -> JsonPrimitive(this.epochSeconds)
is AuthException -> toJsonElement()
+ is AWSCognitoAuthSignInOptions -> toJsonElement()
is CognitoIdentityProviderException -> Json.encodeToJsonElement(
CognitoIdentityProviderExceptionSerializer,
this
diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/authstategenerators/AuthStateJsonGenerator.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/authstategenerators/AuthStateJsonGenerator.kt
index 538d3bddb5..d9dce558d4 100644
--- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/authstategenerators/AuthStateJsonGenerator.kt
+++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/authstategenerators/AuthStateJsonGenerator.kt
@@ -101,5 +101,26 @@ object AuthStateJsonGenerator : SerializableProvider {
AuthorizationState.SigningIn()
)
+ private val receivedCustomChallengeState = AuthState.Configured(
+ AuthenticationState.SigningIn(
+ SignInState.ResolvingChallenge(
+ SignInChallengeState.WaitingForAnswer(
+ AuthChallenge(
+ challengeName = "CUSTOM_CHALLENGE",
+ username = username,
+ session = "someSession",
+ parameters = mapOf(
+ "SALT" to "abc",
+ "SECRET_BLOCK" to "secretBlock",
+ "SRP_B" to "def",
+ "USERNAME" to "username"
+ )
+ )
+ )
+ )
+ ),
+ AuthorizationState.SigningIn()
+ )
+
override val serializables: List = listOf(signedInState, signedOutState, receivedChallengeState)
}
diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignInTestCaseGenerator.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignInTestCaseGenerator.kt
index 7c01c3ec39..1ef909297e 100644
--- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignInTestCaseGenerator.kt
+++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignInTestCaseGenerator.kt
@@ -27,6 +27,7 @@ import com.amplifyframework.auth.cognito.featuretest.ResponseType
import com.amplifyframework.auth.cognito.featuretest.generators.SerializableProvider
import com.amplifyframework.auth.cognito.featuretest.generators.authstategenerators.AuthStateJsonGenerator
import com.amplifyframework.auth.cognito.featuretest.generators.toJsonElement
+import com.amplifyframework.auth.cognito.options.AuthFlowType
import kotlinx.serialization.json.JsonObject
object SignInTestCaseGenerator : SerializableProvider {
@@ -51,6 +52,21 @@ object SignInTestCaseGenerator : SerializableProvider {
).toJsonElement()
)
+ private val mockedInitiateAuthForCustomAuthWithoutSRPResponse = MockResponse(
+ CognitoType.CognitoIdentityProvider,
+ "initiateAuth",
+ ResponseType.Success,
+ mapOf(
+ "challengeName" to ChallengeNameType.CustomChallenge.toString(),
+ "challengeParameters" to mapOf(
+ "SALT" to "abc",
+ "SECRET_BLOCK" to "secretBlock",
+ "SRP_B" to "def",
+ "USERNAME" to username,
+ )
+ ).toJsonElement()
+ )
+
private val mockedRespondToAuthChallengeResponse = MockResponse(
CognitoType.CognitoIdentityProvider,
"respondToAuthChallenge",
@@ -97,6 +113,19 @@ object SignInTestCaseGenerator : SerializableProvider {
).toJsonElement()
)
+ private val mockedCustomChallengeResponse = MockResponse(
+ CognitoType.CognitoIdentityProvider,
+ "respondToAuthChallenge",
+ ResponseType.Success,
+ mapOf(
+ "session" to "someSession",
+ "challengeName" to "CUSTOM_CHALLENGE",
+ "challengeParameters" to mapOf(
+ "Code" to "1234"
+ )
+ ).toJsonElement()
+ )
+
private val mockedIdentityIdResponse = MockResponse(
CognitoType.CognitoIdentity,
"getId",
@@ -146,6 +175,23 @@ object SignInTestCaseGenerator : SerializableProvider {
).toJsonElement()
)
+ private val mockedSignInCustomAuthChallengeExpectation = ExpectationShapes.Amplify(
+ apiName = AuthAPI.signIn,
+ responseType = ResponseType.Success,
+ response = mapOf(
+ "isSignedIn" to false,
+ "nextStep" to mapOf(
+ "signInStep" to "CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE",
+ "additionalInfo" to mapOf(
+ "SALT" to "abc",
+ "SECRET_BLOCK" to "secretBlock",
+ "USERNAME" to "username",
+ "SRP_B" to "def"
+ )
+ )
+ ).toJsonElement()
+ )
+
private val mockConfirmDeviceResponse = MockResponse(
CognitoType.CognitoIdentityProvider,
"confirmDevice",
@@ -174,16 +220,6 @@ object SignInTestCaseGenerator : SerializableProvider {
options = JsonObject(emptyMap())
),
validations = listOf(
-// ExpectationShapes.Cognito(
-// apiName = "signIn",
-// // see [https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_InitiateAuth.html]
-// // see [https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_RespondToAuthChallenge.html]
-// request = mapOf(
-// "clientId" to "testAppClientId", // This should be pulled from configuration
-// "authFlow" to AuthFlowType.UserSrpAuth,
-// "authParameters" to mapOf("username" to username, "SRP_A" to "123")
-// ).toJsonElement()
-// ),
mockedSignInSuccessExpectation,
ExpectationShapes.State("SignedIn_SessionEstablished.json")
)
@@ -232,5 +268,32 @@ object SignInTestCaseGenerator : SerializableProvider {
)
)
- override val serializables: List = listOf(baseCase, challengeCase, deviceSRPTestCase)
+ private val customAuthCase = FeatureTestCase(
+ description = "Test that Custom Auth signIn invokes proper cognito request and returns custom challenge",
+ preConditions = PreConditions(
+ "authconfiguration.json",
+ "SignedOut_Configured.json",
+ mockedResponses = listOf(
+ mockedInitiateAuthForCustomAuthWithoutSRPResponse,
+ mockedCustomChallengeResponse
+ )
+ ),
+ api = API(
+ AuthAPI.signIn,
+ params = mapOf(
+ "username" to username,
+ "password" to "",
+ ).toJsonElement(),
+ options = mapOf(
+ "signInOptions" to
+ mapOf("authFlow" to AuthFlowType.CUSTOM_AUTH_WITHOUT_SRP.toString())
+ ).toJsonElement()
+ ),
+ validations = listOf(
+ mockedSignInCustomAuthChallengeExpectation,
+ ExpectationShapes.State("CustomSignIn_SigningIn.json")
+ )
+ )
+
+ override val serializables: List = listOf(baseCase, challengeCase, deviceSRPTestCase, customAuthCase)
}
diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignUpTestCaseGenerator.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignUpTestCaseGenerator.kt
index 2061df71db..b60ee1fb73 100644
--- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignUpTestCaseGenerator.kt
+++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/featuretest/generators/testcasegenerators/SignUpTestCaseGenerator.kt
@@ -42,6 +42,12 @@ object SignUpTestCaseGenerator : SerializableProvider {
"attributeName" to "attributeName"
)
+ private val emptyCodeDeliveryDetails = mapOf(
+ "destination" to "",
+ "deliveryMedium" to "",
+ "attributeName" to ""
+ )
+
val baseCase = FeatureTestCase(
description = "Test that signup invokes proper cognito request and returns success",
preConditions = PreConditions(
@@ -97,5 +103,45 @@ object SignUpTestCaseGenerator : SerializableProvider {
)
)
- override val serializables: List = listOf(baseCase)
+ val signupSuccessCase = baseCase.copy(
+ description = "Sign up finishes if user is confirmed in the first step",
+ preConditions = baseCase.preConditions.copy(
+ mockedResponses = listOf(
+ MockResponse(
+ CognitoType.CognitoIdentityProvider,
+ "signUp",
+ ResponseType.Success,
+ mapOf("codeDeliveryDetails" to emptyCodeDeliveryDetails, "userConfirmed" to true).toJsonElement()
+ )
+ )
+ ),
+ validations = listOf(
+ ExpectationShapes.Cognito.CognitoIdentityProvider(
+ apiName = "signUp",
+ // see [https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_SignUp.html]
+ request = mapOf(
+ "clientId" to "testAppClientId", // This should be pulled from configuration
+ "username" to username,
+ "password" to password,
+ "userAttributes" to listOf(mapOf("name" to "email", "value" to email))
+ ).toJsonElement()
+ ),
+ ExpectationShapes.Amplify(
+ apiName = AuthAPI.signUp,
+ responseType = ResponseType.Success,
+ response =
+ AuthSignUpResult(
+ true,
+ AuthNextSignUpStep(
+ AuthSignUpStep.DONE,
+ emptyMap(),
+ null
+ ),
+ null
+ ).toJsonElement()
+ )
+ )
+ )
+
+ override val serializables: List = listOf(baseCase, signupSuccessCase)
}
diff --git a/aws-auth-cognito/src/test/java/featureTest/utilities/AuthOptionsFactory.kt b/aws-auth-cognito/src/test/java/featureTest/utilities/AuthOptionsFactory.kt
index 3a80d136f4..0c386a6690 100644
--- a/aws-auth-cognito/src/test/java/featureTest/utilities/AuthOptionsFactory.kt
+++ b/aws-auth-cognito/src/test/java/featureTest/utilities/AuthOptionsFactory.kt
@@ -21,6 +21,8 @@ import com.amplifyframework.auth.cognito.featuretest.AuthAPI
import com.amplifyframework.auth.cognito.featuretest.AuthAPI.resetPassword
import com.amplifyframework.auth.cognito.featuretest.AuthAPI.signIn
import com.amplifyframework.auth.cognito.featuretest.AuthAPI.signUp
+import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions
+import com.amplifyframework.auth.cognito.options.AuthFlowType
import com.amplifyframework.auth.options.AuthConfirmResetPasswordOptions
import com.amplifyframework.auth.options.AuthConfirmSignInOptions
import com.amplifyframework.auth.options.AuthConfirmSignUpOptions
@@ -59,7 +61,7 @@ object AuthOptionsFactory {
AuthAPI.rememberDevice -> TODO()
AuthAPI.resendSignUpCode -> AuthResendSignUpCodeOptions.defaults()
AuthAPI.resendUserAttributeConfirmationCode -> AuthResendUserAttributeConfirmationCodeOptions.defaults()
- signIn -> AuthSignInOptions.defaults()
+ signIn -> getSignInOptions(optionsData)
AuthAPI.signInWithSocialWebUI -> AuthWebUISignInOptions.builder().build()
AuthAPI.signInWithWebUI -> AuthWebUISignInOptions.builder().build()
AuthAPI.signOut -> getSignOutOptions(optionsData)
@@ -74,6 +76,17 @@ object AuthOptionsFactory {
AuthAPI.getVersion -> TODO()
} as T
+ private fun getSignInOptions(optionsData: JsonObject): AuthSignInOptions {
+ return if (optionsData.containsKey("signInOptions")) {
+ val authFlowType = AuthFlowType.valueOf(
+ ((optionsData["signInOptions"] as Map)["authFlow"] as JsonPrimitive).content
+ )
+ AWSCognitoAuthSignInOptions.builder().authFlowType(authFlowType).build()
+ } else {
+ AuthSignInOptions.defaults()
+ }
+ }
+
private fun getSignUpOptions(optionsData: JsonObject): AuthSignUpOptions =
AuthSignUpOptions.builder().userAttributes(
(optionsData["userAttributes"] as Map).map {
diff --git a/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt b/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt
index a3cb1f065d..de657cb440 100644
--- a/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt
+++ b/aws-auth-cognito/src/test/java/featureTest/utilities/CognitoMockFactory.kt
@@ -42,6 +42,7 @@ import io.mockk.coEvery
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.boolean
/**
* Factory to mock aws sdk's cognito API calls and responses.
@@ -67,6 +68,9 @@ class CognitoMockFactory(
setupError(mockResponse, responseObject)
SignUpResponse.invoke {
this.codeDeliveryDetails = parseCodeDeliveryDetails(responseObject)
+ this.userConfirmed = if (responseObject.containsKey("userConfirmed")) {
+ (responseObject["userConfirmed"] as? JsonPrimitive)?.boolean ?: false
+ } else false
}
}
}
diff --git a/aws-auth-cognito/src/test/resources/feature-test/states/CustomSignIn_SigningIn.json b/aws-auth-cognito/src/test/resources/feature-test/states/CustomSignIn_SigningIn.json
new file mode 100644
index 0000000000..e69e8f6662
--- /dev/null
+++ b/aws-auth-cognito/src/test/resources/feature-test/states/CustomSignIn_SigningIn.json
@@ -0,0 +1,26 @@
+{
+ "type": "AuthState.Configured",
+ "AuthenticationState": {
+ "type": "AuthenticationState.SigningIn",
+ "SignInState": {
+ "type": "SignInState.ResolvingChallenge",
+ "SignInChallengeState": {
+ "type": "SignInChallengeState.WaitingForAnswer",
+ "authChallenge": {
+ "challengeName": "CUSTOM_CHALLENGE",
+ "username": "username",
+ "session": null,
+ "parameters": {
+ "SALT": "abc",
+ "SECRET_BLOCK": "secretBlock",
+ "SRP_B": "def",
+ "USERNAME": "username"
+ }
+ }
+ }
+ }
+ },
+ "AuthorizationState": {
+ "type": "AuthorizationState.SigningIn"
+ }
+}
diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/AuthSession_object_is_successfully_returned_after_refresh.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/AuthSession_object_is_successfully_returned_after_refresh.json
new file mode 100644
index 0000000000..b10484548e
--- /dev/null
+++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/AuthSession_object_is_successfully_returned_after_refresh.json
@@ -0,0 +1,53 @@
+{
+ "description": "AuthSession object is successfully returned after refresh",
+ "preConditions": {
+ "amplify-configuration": "authconfiguration.json",
+ "state": "SignedIn_SessionEstablished.json",
+ "mockedResponses": [
+ {
+ "type": "cognitoIdentityProvider",
+ "apiName": "initiateAuth",
+ "responseType": "success",
+ "response": {
+ "authenticationResult": {
+ "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
+ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
+ "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
+ "expiresIn": 300
+ }
+ }
+ }
+ ]
+ },
+ "api": {
+ "name": "fetchAuthSession",
+ "params": {
+ },
+ "options": {
+ "forceRefresh": true
+ }
+ },
+ "validations": [
+ {
+ "type": "amplify",
+ "apiName": "fetchAuthSession",
+ "responseType": "success",
+ "response": {
+ "awsCredentialsResult": {
+ "accessKeyId": "someAccessKey",
+ "expiration": 2342134,
+ "secretAccessKey": "someSecretKey",
+ "sessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU"
+ },
+ "identityIdResult": "someIdentityId",
+ "isSignedIn": true,
+ "userPoolTokensResult": {
+ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
+ "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
+ "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU"
+ },
+ "userSubResult": "userId"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/AuthSession_object_is_successfully_returned_for_Identity_Pool.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/AuthSession_object_is_successfully_returned_for_Identity_Pool.json
new file mode 100644
index 0000000000..98d7623119
--- /dev/null
+++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/AuthSession_object_is_successfully_returned_for_Identity_Pool.json
@@ -0,0 +1,45 @@
+{
+ "description": "AuthSession object is successfully returned for Identity Pool",
+ "preConditions": {
+ "amplify-configuration": "authconfiguration.json",
+ "state": "SignedOut_IdentityPoolConfigured.json",
+ "mockedResponses": [
+ ]
+ },
+ "api": {
+ "name": "fetchAuthSession",
+ "params": {
+ },
+ "options": {
+ }
+ },
+ "validations": [
+ {
+ "type": "amplify",
+ "apiName": "fetchAuthSession",
+ "responseType": "success",
+ "response": {
+ "awsCredentialsResult": {
+ "accessKeyId": "someAccessKey",
+ "expiration": 2342134,
+ "secretAccessKey": "someSecretKey",
+ "sessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU"
+ },
+ "identityIdResult": "someIdentityId",
+ "isSignedIn": false,
+ "userPoolTokensResult": {
+ "errorType": "SignedOutException",
+ "errorMessage": "You are currently signed out.",
+ "recoverySuggestion": "Please sign in and reattempt the operation.",
+ "cause": null
+ },
+ "userSubResult": {
+ "errorType": "SignedOutException",
+ "errorMessage": "You are currently signed out.",
+ "recoverySuggestion": "Please sign in and reattempt the operation.",
+ "cause": null
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/Test_that_API_is_called_with_given_payload_and_returns_successful_data.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/AuthSession_object_is_successfully_returned_for_UserAndIdentity_Pool.json
similarity index 95%
rename from aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/Test_that_API_is_called_with_given_payload_and_returns_successful_data.json
rename to aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/AuthSession_object_is_successfully_returned_for_UserAndIdentity_Pool.json
index aacf3ab58c..5c840db23c 100644
--- a/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/Test_that_API_is_called_with_given_payload_and_returns_successful_data.json
+++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/AuthSession_object_is_successfully_returned_for_UserAndIdentity_Pool.json
@@ -1,5 +1,5 @@
{
- "description": "Test that API is called with given payload and returns successful data",
+ "description": "AuthSession object is successfully returned for UserAndIdentity Pool",
"preConditions": {
"amplify-configuration": "authconfiguration.json",
"state": "SignedIn_SessionEstablished.json",
diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/AuthSession_object_is_successfully_returned_for_User_Pool.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/AuthSession_object_is_successfully_returned_for_User_Pool.json
new file mode 100644
index 0000000000..58db064009
--- /dev/null
+++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/fetchAuthSession/AuthSession_object_is_successfully_returned_for_User_Pool.json
@@ -0,0 +1,44 @@
+{
+ "description": "AuthSession object is successfully returned for User Pool",
+ "preConditions": {
+ "amplify-configuration": "authconfiguration.json",
+ "state": "SignedIn_UserPoolSessionEstablished.json",
+ "mockedResponses": [
+ ]
+ },
+ "api": {
+ "name": "fetchAuthSession",
+ "params": {
+ },
+ "options": {
+ }
+ },
+ "validations": [
+ {
+ "type": "amplify",
+ "apiName": "fetchAuthSession",
+ "responseType": "success",
+ "response": {
+ "awsCredentialsResult": {
+ "errorType": "ConfigurationException",
+ "errorMessage": "Could not fetch AWS Cognito credentials",
+ "recoverySuggestion": "Cognito Identity not configured. Please check amplifyconfiguration.json file.",
+ "cause": null
+ },
+ "identityIdResult": {
+ "errorType": "ConfigurationException",
+ "errorMessage": "Could not retrieve Identity ID",
+ "recoverySuggestion": "Cognito Identity not configured. Please check amplifyconfiguration.json file.",
+ "cause": null
+ },
+ "isSignedIn": true,
+ "userPoolTokensResult": {
+ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
+ "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
+ "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU"
+ },
+ "userSubResult": "userId"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_Device_SRP_signIn_invokes_proper_cognito_request_and_returns_success.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_Device_SRP_signIn_invokes_proper_cognito_request_and_returns_success.json
deleted file mode 100644
index 6631e63727..0000000000
--- a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_Device_SRP_signIn_invokes_proper_cognito_request_and_returns_success.json
+++ /dev/null
@@ -1,97 +0,0 @@
-{
- "description": "Test that Device SRP signIn invokes proper cognito request and returns success",
- "preConditions": {
- "amplify-configuration": "authconfiguration.json",
- "state": "SignedOut_Configured.json",
- "mockedResponses": [
- {
- "type": "cognitoIdentityProvider",
- "apiName": "initiateAuth",
- "responseType": "success",
- "response": {
- "challengeName": "PASSWORD_VERIFIER",
- "challengeParameters": {
- "SALT": "abc",
- "SECRET_BLOCK": "secretBlock",
- "SRP_B": "def",
- "USERNAME": "username",
- "USER_ID_FOR_SRP": "userId"
- }
- }
- },
- {
- "type": "cognitoIdentityProvider",
- "apiName": "respondToAuthChallenge",
- "responseType": "success",
- "response": {
- "authenticationResult": {
- "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
- "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
- "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
- "expiresIn": 300,
- "newDeviceMetadata": {
- "deviceKey": "someDeviceKey",
- "deviceGroupKey": "someDeviceGroupKey"
- }
- }
- }
- },
- {
- "type": "cognitoIdentityProvider",
- "apiName": "confirmDevice",
- "responseType": "success",
- "response": {
- }
- },
- {
- "type": "cognitoIdentity",
- "apiName": "getId",
- "responseType": "success",
- "response": {
- "identityId": "someIdentityId"
- }
- },
- {
- "type": "cognitoIdentity",
- "apiName": "getCredentialsForIdentity",
- "responseType": "success",
- "response": {
- "credentials": {
- "accessKeyId": "someAccessKey",
- "secretKey": "someSecretKey",
- "sessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
- "expiration": 2342134
- }
- }
- }
- ]
- },
- "api": {
- "name": "signIn",
- "params": {
- "username": "username",
- "password": "password"
- },
- "options": {
- }
- },
- "validations": [
- {
- "type": "amplify",
- "apiName": "signIn",
- "responseType": "success",
- "response": {
- "isSignedIn": true,
- "nextStep": {
- "signInStep": "DONE",
- "additionalInfo": {
- }
- }
- }
- },
- {
- "type": "state",
- "expectedState": "SignedIn_SessionEstablished.json"
- }
- ]
-}
\ No newline at end of file
diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_SRP_signIn_invokes_proper_cognito_request_and_returns_SMS_challenge.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_SRP_signIn_invokes_proper_cognito_request_and_returns_SMS_challenge.json
deleted file mode 100644
index 5cc1ab4d0a..0000000000
--- a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_SRP_signIn_invokes_proper_cognito_request_and_returns_SMS_challenge.json
+++ /dev/null
@@ -1,69 +0,0 @@
-{
- "description": "Test that SRP signIn invokes proper cognito request and returns SMS challenge",
- "preConditions": {
- "amplify-configuration": "authconfiguration.json",
- "state": "SignedOut_Configured.json",
- "mockedResponses": [
- {
- "type": "cognitoIdentityProvider",
- "apiName": "initiateAuth",
- "responseType": "success",
- "response": {
- "challengeName": "PASSWORD_VERIFIER",
- "challengeParameters": {
- "SALT": "abc",
- "SECRET_BLOCK": "secretBlock",
- "SRP_B": "def",
- "USERNAME": "username",
- "USER_ID_FOR_SRP": "userId"
- }
- }
- },
- {
- "type": "cognitoIdentityProvider",
- "apiName": "respondToAuthChallenge",
- "responseType": "success",
- "response": {
- "session": "someSession",
- "challengeName": "SMS_MFA",
- "challengeParameters": {
- "CODE_DELIVERY_DELIVERY_MEDIUM": "SMS",
- "CODE_DELIVERY_DESTINATION": "+12345678900"
- }
- }
- }
- ]
- },
- "api": {
- "name": "signIn",
- "params": {
- "username": "username",
- "password": "password"
- },
- "options": {
- }
- },
- "validations": [
- {
- "type": "amplify",
- "apiName": "signIn",
- "responseType": "success",
- "response": {
- "isSignedIn": false,
- "nextStep": {
- "signInStep": "CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE",
- "additionalInfo": {
- },
- "codeDeliveryDetails": {
- "destination": "+12345678900",
- "deliveryMedium": "SMS"
- }
- }
- }
- },
- {
- "type": "state",
- "expectedState": "SigningIn_SigningIn.json"
- }
- ]
-}
\ No newline at end of file
diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_SRP_signIn_invokes_proper_cognito_request_and_returns_success.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_SRP_signIn_invokes_proper_cognito_request_and_returns_success.json
deleted file mode 100644
index 364b080df8..0000000000
--- a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signIn/Test_that_SRP_signIn_invokes_proper_cognito_request_and_returns_success.json
+++ /dev/null
@@ -1,86 +0,0 @@
-{
- "description": "Test that SRP signIn invokes proper cognito request and returns success",
- "preConditions": {
- "amplify-configuration": "authconfiguration.json",
- "state": "SignedOut_Configured.json",
- "mockedResponses": [
- {
- "type": "cognitoIdentityProvider",
- "apiName": "initiateAuth",
- "responseType": "success",
- "response": {
- "challengeName": "PASSWORD_VERIFIER",
- "challengeParameters": {
- "SALT": "abc",
- "SECRET_BLOCK": "secretBlock",
- "SRP_B": "def",
- "USERNAME": "username",
- "USER_ID_FOR_SRP": "userId"
- }
- }
- },
- {
- "type": "cognitoIdentityProvider",
- "apiName": "respondToAuthChallenge",
- "responseType": "success",
- "response": {
- "authenticationResult": {
- "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
- "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
- "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
- "expiresIn": 300
- }
- }
- },
- {
- "type": "cognitoIdentity",
- "apiName": "getId",
- "responseType": "success",
- "response": {
- "identityId": "someIdentityId"
- }
- },
- {
- "type": "cognitoIdentity",
- "apiName": "getCredentialsForIdentity",
- "responseType": "success",
- "response": {
- "credentials": {
- "accessKeyId": "someAccessKey",
- "secretKey": "someSecretKey",
- "sessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoib3JpZ2luX2p0aSJ9.Xqa-vjJe5wwwsqeRAdHf8kTBn_rYSkDn2lB7xj9Z1xU",
- "expiration": 2342134
- }
- }
- }
- ]
- },
- "api": {
- "name": "signIn",
- "params": {
- "username": "username",
- "password": "password"
- },
- "options": {
- }
- },
- "validations": [
- {
- "type": "amplify",
- "apiName": "signIn",
- "responseType": "success",
- "response": {
- "isSignedIn": true,
- "nextStep": {
- "signInStep": "DONE",
- "additionalInfo": {
- }
- }
- }
- },
- {
- "type": "state",
- "expectedState": "SignedIn_SessionEstablished.json"
- }
- ]
-}
\ No newline at end of file
diff --git a/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json
new file mode 100644
index 0000000000..156fa4a187
--- /dev/null
+++ b/aws-auth-cognito/src/test/resources/feature-test/testsuites/signUp/Sign_up_finishes_if_user_is_confirmed_in_the_first_step.json
@@ -0,0 +1,64 @@
+{
+ "description": "Sign up finishes if user is confirmed in the first step",
+ "preConditions": {
+ "amplify-configuration": "authconfiguration.json",
+ "state": "SignedOut_Configured.json",
+ "mockedResponses": [
+ {
+ "type": "cognitoIdentityProvider",
+ "apiName": "signUp",
+ "responseType": "success",
+ "response": {
+ "codeDeliveryDetails": {
+ "destination": "",
+ "deliveryMedium": "",
+ "attributeName": ""
+ },
+ "userConfirmed": true
+ }
+ }
+ ]
+ },
+ "api": {
+ "name": "signUp",
+ "params": {
+ "username": "user",
+ "password": "password"
+ },
+ "options": {
+ "userAttributes": {
+ "email": "user@domain.com"
+ }
+ }
+ },
+ "validations": [
+ {
+ "type": "cognitoIdentityProvider",
+ "apiName": "signUp",
+ "request": {
+ "clientId": "testAppClientId",
+ "username": "user",
+ "password": "password",
+ "userAttributes": [
+ {
+ "name": "email",
+ "value": "user@domain.com"
+ }
+ ]
+ }
+ },
+ {
+ "type": "amplify",
+ "apiName": "signUp",
+ "responseType": "success",
+ "response": {
+ "isSignUpComplete": true,
+ "nextStep": {
+ "signUpStep": "DONE",
+ "additionalInfo": {
+ }
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StorageDownloadTest.java b/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StorageDownloadTest.java
index 0b1dddb4aa..41fc7d9729 100644
--- a/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StorageDownloadTest.java
+++ b/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StorageDownloadTest.java
@@ -59,7 +59,7 @@
* Instrumentation test for operational work on download.
*/
public final class AWSS3StorageDownloadTest {
- private static final long EXTENDED_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(20);
+ private static final long EXTENDED_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(60);
private static final StorageAccessLevel TESTING_ACCESS_LEVEL = StorageAccessLevel.PUBLIC;
private static final long LARGE_FILE_SIZE = 10 * 1024 * 1024L; // 10 MB
diff --git a/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StorageUploadTest.java b/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StorageUploadTest.java
index 9fd05dc57e..7199f81fb2 100644
--- a/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StorageUploadTest.java
+++ b/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/AWSS3StorageUploadTest.java
@@ -60,7 +60,7 @@
* Instrumentation test for operational work on upload.
*/
public final class AWSS3StorageUploadTest {
- private static final long EXTENDED_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(20);
+ private static final long EXTENDED_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(60);
private static final StorageAccessLevel TESTING_ACCESS_LEVEL = StorageAccessLevel.PUBLIC;
private static final long LARGE_FILE_SIZE = 10 * 1024 * 1024L; // 10 MB
diff --git a/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/transfer/TransferDBTest.kt b/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/transfer/TransferDBTest.kt
index af7bdb3c29..4937f389f0 100644
--- a/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/transfer/TransferDBTest.kt
+++ b/aws-storage-s3/src/androidTest/java/com/amplifyframework/storage/s3/transfer/TransferDBTest.kt
@@ -19,7 +19,10 @@ import android.content.ContentValues
import android.content.Context
import android.net.Uri
import androidx.test.core.app.ApplicationProvider
+import com.amplifyframework.storage.ObjectMetadata
import java.io.File
+import java.sql.Date
+import java.time.Instant
import java.util.UUID
import org.junit.After
import org.junit.Assert
@@ -91,6 +94,41 @@ open class TransferDBTest {
} ?: Assert.fail("InsertedRecord is null")
}
+ @Test
+ fun generateContentValuesForMultiPartUploadWithMetadata() {
+ val key = UUID.randomUUID().toString()
+ val expectedHttpExpiresDate = Date.from(Instant.now())
+ val expectedExpirationDate = Date.from(Instant.EPOCH)
+ val restoreExpirationTime = Date.from(Instant.EPOCH)
+ val contentValues = arrayOfNulls(1)
+ contentValues[0] = transferDB.generateContentValuesForMultiPartUpload(
+ key,
+ bucketName,
+ key,
+ tempFile,
+ 0L,
+ 0,
+ null,
+ 1L,
+ 0,
+ ObjectMetadata(
+ userMetadata = mapOf("key1" to "value1"),
+ metaData = mutableMapOf("key1" to "value1"),
+ httpExpiresDate = expectedHttpExpiresDate,
+ expirationTime = expectedExpirationDate,
+ expirationTimeRuleId = "ruleId",
+ ongoingRestore = false,
+ restoreExpirationTime = restoreExpirationTime
+ ),
+ null
+ )
+ val uri = transferDB.bulkInsertTransferRecords(contentValues)
+ transferDB.getTransferRecordById(uri).run {
+ Assert.assertEquals(mapOf("key1" to "value1"), this?.userMetadata)
+ Assert.assertNull(this?.headerStorageClass)
+ }
+ }
+
@Test
fun testMultiPartDelete() {
val key = UUID.randomUUID().toString()
diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/TransferOperations.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/TransferOperations.kt
index 6ec103dc00..47cd602c41 100644
--- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/TransferOperations.kt
+++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/TransferOperations.kt
@@ -82,8 +82,8 @@ internal object TransferOperations {
workManager: WorkManager
): Boolean {
if (TransferState.isStarted(transferRecord.state) && !TransferState.isInTerminalState(transferRecord.state)) {
- workManager.cancelUniqueWork(transferRecord.id.toString())
transferStatusUpdater.updateTransferState(transferRecord.id, TransferState.PENDING_PAUSE)
+ workManager.cancelUniqueWork(transferRecord.id.toString())
return true
}
return false
@@ -122,10 +122,11 @@ internal object TransferOperations {
// transfer is paused so directly mark it as canceled
nextState = TransferState.CANCELED
}
+ transferStatusUpdater.updateTransferState(transferRecord.id, nextState)
} else {
+ transferStatusUpdater.updateTransferState(transferRecord.id, nextState)
workManager.cancelUniqueWork(transferRecord.id.toString())
}
- transferStatusUpdater.updateTransferState(transferRecord.id, nextState)
return true
}
return false
diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/TransferDB.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/TransferDB.kt
index f069908202..09fa778a4f 100644
--- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/TransferDB.kt
+++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/TransferDB.kt
@@ -745,23 +745,41 @@ internal class TransferDB private constructor(context: Context) {
* with
* @return the ContentValues
*/
- private fun generateContentValuesForObjectMetadata(metadata: ObjectMetadata?): ContentValues? {
+ private fun generateContentValuesForObjectMetadata(metadata: ObjectMetadata?): ContentValues {
val values = ContentValues()
metadata?.let {
values.apply {
- TransferTable.COLUMN_USER_METADATA to JsonUtils.mapToString(it.userMetadata)
- TransferTable.COLUMN_HEADER_CONTENT_TYPE to it.metaData[ObjectMetadata.CONTENT_TYPE].toString()
- TransferTable.COLUMN_HEADER_CONTENT_ENCODING to it.metaData[ObjectMetadata.CONTENT_ENCODING].toString()
- TransferTable.COLUMN_HEADER_CACHE_CONTROL to it.metaData[ObjectMetadata.CACHE_CONTROL].toString()
- TransferTable.COLUMN_CONTENT_MD5 to it.metaData[ObjectMetadata.CONTENT_MD5].toString()
- TransferTable.COLUMN_HEADER_CONTENT_DISPOSITION to
- it.metaData[ObjectMetadata.CONTENT_DISPOSITION].toString()
- TransferTable.COLUMN_SSE_ALGORITHM to it.metaData[ObjectMetadata.SERVER_SIDE_ENCRYPTION].toString()
- TransferTable.COLUMN_SSE_KMS_KEY to
- it.metaData[ObjectMetadata.SERVER_SIDE_ENCRYPTION_KMS_KEY_ID].toString()
- TransferTable.COLUMN_EXPIRATION_TIME_RULE_ID to it.expirationTimeRuleId
- TransferTable.COLUMN_HTTP_EXPIRES_DATE to it.httpExpiresDate?.time.toString()
- TransferTable.COLUMN_HEADER_STORAGE_CLASS to it.metaData[ObjectMetadata.STORAGE_CLASS].toString()
+ put(TransferTable.COLUMN_USER_METADATA, JsonUtils.mapToString(it.userMetadata))
+ it.metaData[ObjectMetadata.CONTENT_TYPE]?.let {
+ put(TransferTable.COLUMN_HEADER_CONTENT_TYPE, it.toString())
+ }
+ it.metaData[ObjectMetadata.CONTENT_ENCODING]?.let {
+ put(TransferTable.COLUMN_HEADER_CONTENT_ENCODING, it.toString())
+ }
+ it.metaData[ObjectMetadata.CACHE_CONTROL]?.let {
+ put(TransferTable.COLUMN_HEADER_CACHE_CONTROL, it.toString())
+ }
+ it.metaData[ObjectMetadata.CONTENT_MD5]?.let {
+ put(TransferTable.COLUMN_CONTENT_MD5, it.toString())
+ }
+ it.metaData[ObjectMetadata.CONTENT_DISPOSITION]?.let {
+ put(TransferTable.COLUMN_HEADER_CONTENT_DISPOSITION, it.toString())
+ }
+ it.metaData[ObjectMetadata.SERVER_SIDE_ENCRYPTION]?.let {
+ put(TransferTable.COLUMN_SSE_ALGORITHM, it.toString())
+ }
+ it.metaData[ObjectMetadata.SERVER_SIDE_ENCRYPTION_KMS_KEY_ID]?.let {
+ put(TransferTable.COLUMN_SSE_KMS_KEY, it.toString())
+ }
+ it.expirationTimeRuleId?.let {
+ put(TransferTable.COLUMN_EXPIRATION_TIME_RULE_ID, it)
+ }
+ it.httpExpiresDate?.let {
+ put(TransferTable.COLUMN_HTTP_EXPIRES_DATE, it.time)
+ }
+ it.metaData[ObjectMetadata.STORAGE_CLASS]?.let {
+ put(TransferTable.COLUMN_HEADER_STORAGE_CLASS, it.toString())
+ }
}
}
return values
diff --git a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/DownloadWorker.kt b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/DownloadWorker.kt
index 225dde2b28..4222153877 100644
--- a/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/DownloadWorker.kt
+++ b/aws-storage-s3/src/main/java/com/amplifyframework/storage/s3/transfer/worker/DownloadWorker.kt
@@ -27,7 +27,8 @@ import com.amplifyframework.storage.s3.transfer.TransferStatusUpdater
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
/**
* Worker to perform download file task.
@@ -73,21 +74,19 @@ internal class DownloadWorker(
}
@OptIn(InternalApi::class)
- private fun writeToFileWithProgressUpdates(
+ private suspend fun writeToFileWithProgressUpdates(
stream: ByteStream.OneShotStream,
file: File,
progressListener: DownloadProgressListener
) {
- var outputStream: BufferedOutputStream? = null
- val sdkByteReadChannel = stream.readFrom()
- runBlocking {
+ withContext(Dispatchers.IO) {
+ val sdkByteReadChannel = stream.readFrom()
val limit = stream.contentLength ?: 0L
val buffer = ByteArray(defaultBufferSize)
val append = file.length() > 0
val fileOutputStream = FileOutputStream(file, append)
- outputStream = BufferedOutputStream(fileOutputStream)
var totalRead = 0L
- outputStream?.use { fileOutput ->
+ BufferedOutputStream(fileOutputStream).use { fileOutput ->
val copied = 0L
while (!isStopped) {
val remaining = limit - copied
diff --git a/configuration/instrumentation-tests.gradle b/configuration/instrumentation-tests.gradle
index 0a4eab90c9..dd552b5e81 100644
--- a/configuration/instrumentation-tests.gradle
+++ b/configuration/instrumentation-tests.gradle
@@ -25,11 +25,11 @@ subprojects {
}
}
}
- task runTestsInDeviceFarmPool {
+ task runNightlyTestsInDeviceFarmPool {
dependsOn(assembleAndroidTest)
doLast {
exec {
- commandLine("$rootDir.path/scripts/run_tests_in_devicefarm_pool.sh")
+ commandLine("$rootDir.path/scripts/run_nightly_tests_in_devicefarm_pool.sh")
args([project.name])
}
}
diff --git a/core-kotlin/src/main/java/com/amplifyframework/kotlin/core/Amplify.kt b/core-kotlin/src/main/java/com/amplifyframework/kotlin/core/Amplify.kt
index 4e50124de5..9d1ecb9b43 100644
--- a/core-kotlin/src/main/java/com/amplifyframework/kotlin/core/Amplify.kt
+++ b/core-kotlin/src/main/java/com/amplifyframework/kotlin/core/Amplify.kt
@@ -24,6 +24,7 @@ import com.amplifyframework.core.plugin.Plugin
import com.amplifyframework.kotlin.api.KotlinApiFacade
import com.amplifyframework.kotlin.auth.KotlinAuthFacade
import com.amplifyframework.kotlin.datastore.KotlinDataStoreFacade
+import com.amplifyframework.kotlin.geo.KotlinGeoFacade
import com.amplifyframework.kotlin.hub.KotlinHubFacade
import com.amplifyframework.kotlin.predictions.KotlinPredictionsFacade
import com.amplifyframework.kotlin.storage.KotlinStorageFacade
@@ -41,6 +42,7 @@ class Amplify {
val Analytics = AnalyticsCategory()
val API = KotlinApiFacade()
val Auth = KotlinAuthFacade()
+ val Geo = KotlinGeoFacade()
val Logging = LoggingCategory()
val Storage = KotlinStorageFacade()
val Hub = KotlinHubFacade()
diff --git a/core-kotlin/src/main/java/com/amplifyframework/kotlin/geo/Geo.kt b/core-kotlin/src/main/java/com/amplifyframework/kotlin/geo/Geo.kt
new file mode 100644
index 0000000000..fffe28e056
--- /dev/null
+++ b/core-kotlin/src/main/java/com/amplifyframework/kotlin/geo/Geo.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 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.kotlin.geo
+
+import com.amplifyframework.geo.models.Coordinates
+import com.amplifyframework.geo.models.MapStyle
+import com.amplifyframework.geo.models.MapStyleDescriptor
+import com.amplifyframework.geo.options.GeoSearchByCoordinatesOptions
+import com.amplifyframework.geo.options.GeoSearchByTextOptions
+import com.amplifyframework.geo.options.GetMapStyleDescriptorOptions
+import com.amplifyframework.geo.result.GeoSearchResult
+
+interface Geo {
+ /**
+ * Gets a collection of maps and their corresponding styles.
+ *
+ * @return A collection of all available [MapStyle].
+ */
+ suspend fun getAvailableMaps(): Collection
+
+ /**
+ * Gets the default map and style from available maps.
+ *
+ * @return The default [MapStyle].
+ */
+ suspend fun getDefaultMap(): MapStyle
+
+ /**
+ * Uses given options to get map style descriptor JSON.
+ *
+ * @param options Options to specify for this operation.
+ * @return The [MapStyleDescriptor] matching the given options.
+ */
+ suspend fun getMapStyleDescriptor(
+ options: GetMapStyleDescriptorOptions = GetMapStyleDescriptorOptions.defaults()
+ ): MapStyleDescriptor
+
+ /**
+ * Searches for locations that match text query.
+ *
+ * @param query Search query text.
+ * @param options Search options to use.
+ * @return The [GeoSearchResult] for the query and options.
+ */
+ suspend fun searchByText(
+ query: String,
+ options: GeoSearchByTextOptions = GeoSearchByTextOptions.defaults()
+ ): GeoSearchResult
+
+ /**
+ * Searches for location with given set of coordinates.
+ *
+ * @param position Coordinates to look-up.
+ * @param options Search options to use.
+ * @return The [GeoSearchResult] for the position and options.
+ */
+ suspend fun searchByCoordinates(
+ position: Coordinates,
+ options: GeoSearchByCoordinatesOptions = GeoSearchByCoordinatesOptions.defaults()
+ ): GeoSearchResult
+}
diff --git a/core-kotlin/src/main/java/com/amplifyframework/kotlin/geo/KotlinGeoFacade.kt b/core-kotlin/src/main/java/com/amplifyframework/kotlin/geo/KotlinGeoFacade.kt
new file mode 100644
index 0000000000..e30163a931
--- /dev/null
+++ b/core-kotlin/src/main/java/com/amplifyframework/kotlin/geo/KotlinGeoFacade.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2022 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.kotlin.geo
+
+import com.amplifyframework.core.Amplify
+import com.amplifyframework.geo.GeoCategoryBehavior
+import com.amplifyframework.geo.models.Coordinates
+import com.amplifyframework.geo.models.MapStyle
+import com.amplifyframework.geo.options.GeoSearchByCoordinatesOptions
+import com.amplifyframework.geo.options.GeoSearchByTextOptions
+import com.amplifyframework.geo.options.GetMapStyleDescriptorOptions
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+class KotlinGeoFacade(private val delegate: GeoCategoryBehavior = Amplify.Geo) : Geo {
+ override suspend fun getAvailableMaps(): Collection = suspendCoroutine { continuation ->
+ delegate.getAvailableMaps(
+ { continuation.resume(it) },
+ { continuation.resumeWithException(it) }
+ )
+ }
+
+ override suspend fun getDefaultMap() = suspendCoroutine { continuation ->
+ delegate.getDefaultMap(
+ { continuation.resume(it) },
+ { continuation.resumeWithException(it) }
+ )
+ }
+
+ override suspend fun getMapStyleDescriptor(options: GetMapStyleDescriptorOptions) =
+ suspendCoroutine { continuation ->
+ delegate.getMapStyleDescriptor(
+ options,
+ { continuation.resume(it) },
+ { continuation.resumeWithException(it) }
+ )
+ }
+
+ override suspend fun searchByText(
+ query: String,
+ options: GeoSearchByTextOptions
+ ) = suspendCoroutine { continuation ->
+ delegate.searchByText(
+ query,
+ options,
+ { continuation.resume(it) },
+ { continuation.resumeWithException(it) }
+ )
+ }
+
+ override suspend fun searchByCoordinates(
+ position: Coordinates,
+ options: GeoSearchByCoordinatesOptions
+ ) = suspendCoroutine { continuation ->
+ delegate.searchByCoordinates(
+ position,
+ options,
+ { continuation.resume(it) },
+ { continuation.resumeWithException(it) }
+ )
+ }
+}
diff --git a/core-kotlin/src/main/java/com/amplifyframework/kotlin/storage/KotlinStorageFacade.kt b/core-kotlin/src/main/java/com/amplifyframework/kotlin/storage/KotlinStorageFacade.kt
index 7d6f77fcf1..ff93bf04e9 100644
--- a/core-kotlin/src/main/java/com/amplifyframework/kotlin/storage/KotlinStorageFacade.kt
+++ b/core-kotlin/src/main/java/com/amplifyframework/kotlin/storage/KotlinStorageFacade.kt
@@ -16,7 +16,6 @@
package com.amplifyframework.kotlin.storage
import com.amplifyframework.core.Amplify
-import com.amplifyframework.core.async.Cancelable
import com.amplifyframework.kotlin.storage.Storage.InProgressStorageOperation
import com.amplifyframework.storage.StorageCategoryBehavior as Delegate
import com.amplifyframework.storage.StorageException
@@ -75,10 +74,10 @@ class KotlinStorageFacade(private val delegate: Delegate = Amplify.Storage) : St
{ errors.tryEmit(it) }
)
return InProgressStorageOperation(
- results.asSharedFlow(),
+ results.transferId,
progress.asSharedFlow(),
errors.asSharedFlow(),
- operation as Cancelable
+ operation
)
}
@@ -98,10 +97,10 @@ class KotlinStorageFacade(private val delegate: Delegate = Amplify.Storage) : St
{ errors.tryEmit(it) }
)
return InProgressStorageOperation(
- results.asSharedFlow(),
+ results.transferId,
progress.asSharedFlow(),
errors.asSharedFlow(),
- operation as Cancelable
+ operation
)
}
@@ -125,10 +124,10 @@ class KotlinStorageFacade(private val delegate: Delegate = Amplify.Storage) : St
{ errors.tryEmit(it) }
)
return InProgressStorageOperation(
- results.asSharedFlow(),
+ results.transferId,
progress.asSharedFlow(),
errors.asSharedFlow(),
- cancelable as Cancelable
+ cancelable
)
}
diff --git a/core-kotlin/src/main/java/com/amplifyframework/kotlin/storage/Storage.kt b/core-kotlin/src/main/java/com/amplifyframework/kotlin/storage/Storage.kt
index 969cab2281..845036603b 100644
--- a/core-kotlin/src/main/java/com/amplifyframework/kotlin/storage/Storage.kt
+++ b/core-kotlin/src/main/java/com/amplifyframework/kotlin/storage/Storage.kt
@@ -16,6 +16,7 @@
package com.amplifyframework.kotlin.storage
import com.amplifyframework.core.async.Cancelable
+import com.amplifyframework.core.async.Resumable
import com.amplifyframework.storage.StorageException
import com.amplifyframework.storage.operation.StorageTransferOperation
import com.amplifyframework.storage.options.StorageDownloadFileOptions
@@ -91,11 +92,12 @@ interface Storage {
@FlowPreview
data class InProgressStorageOperation(
+ val transferId: String,
private val results: Flow,
private val progress: Flow,
private val errors: Flow,
- private val delegate: Cancelable?
- ) : Cancelable {
+ private val delegate: StorageTransferOperation<*, *>?
+ ) : Cancelable, Resumable {
override fun cancel() {
delegate?.cancel()
@@ -120,5 +122,13 @@ interface Storage {
.map { it as T }
.first()
}
+
+ override fun pause() {
+ delegate?.pause()
+ }
+
+ override fun resume() {
+ delegate?.resume()
+ }
}
}
diff --git a/core-kotlin/src/test/java/com/amplifyframework/kotlin/geo/KotlinGeoFacadeTest.kt b/core-kotlin/src/test/java/com/amplifyframework/kotlin/geo/KotlinGeoFacadeTest.kt
new file mode 100644
index 0000000000..0efb5516bb
--- /dev/null
+++ b/core-kotlin/src/test/java/com/amplifyframework/kotlin/geo/KotlinGeoFacadeTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2022 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.kotlin.geo
+
+import com.amplifyframework.core.Consumer
+import com.amplifyframework.geo.GeoCategoryBehavior
+import com.amplifyframework.geo.GeoException
+import com.amplifyframework.geo.models.Coordinates
+import com.amplifyframework.geo.models.MapStyle
+import com.amplifyframework.geo.models.MapStyleDescriptor
+import com.amplifyframework.geo.options.GeoSearchByCoordinatesOptions
+import com.amplifyframework.geo.options.GeoSearchByTextOptions
+import com.amplifyframework.geo.options.GetMapStyleDescriptorOptions
+import com.amplifyframework.geo.result.GeoSearchResult
+import io.mockk.every
+import io.mockk.mockk
+import junit.framework.Assert.assertSame
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+
+/**
+ * Unit tests for the [KotlinGeoFacade] class.
+ */
+internal class KotlinGeoFacadeTest {
+ private val delegate = mockk()
+ private val geo = KotlinGeoFacade(delegate)
+
+ @Test
+ fun `gets available maps`() = runBlocking {
+ val maps = listOf(
+ MapStyle("a", "b"),
+ MapStyle("c", "d")
+ )
+ every { delegate.getAvailableMaps(any(), any()) } answers {
+ val callback = firstArg>>()
+ callback.accept(maps)
+ }
+ val result = geo.getAvailableMaps()
+ assertSame(maps, result)
+ }
+
+ @Test(expected = GeoException::class)
+ fun `throws available map error`(): Unit = runBlocking {
+ val error = GeoException("message", "suggestion")
+ every { delegate.getAvailableMaps(any(), any()) } answers {
+ val callback = secondArg>()
+ callback.accept(error)
+ }
+ geo.getAvailableMaps()
+ }
+
+ @Test
+ fun `gets default map`() = runBlocking {
+ val map = MapStyle("name", "style")
+ every { delegate.getDefaultMap(any(), any()) } answers {
+ val callback = firstArg>()
+ callback.accept(map)
+ }
+ val result = geo.getDefaultMap()
+ assertSame(map, result)
+ }
+
+ @Test(expected = GeoException::class)
+ fun `throws default map error`(): Unit = runBlocking {
+ val error = GeoException("message", "suggestion")
+ every { delegate.getDefaultMap(any(), any()) } answers {
+ val callback = secondArg>()
+ callback.accept(error)
+ }
+ geo.getDefaultMap()
+ }
+
+ @Test
+ fun `returns map style descriptor`() = runBlocking {
+ val descriptor = MapStyleDescriptor("")
+ every { delegate.getMapStyleDescriptor(any(), any(), any()) } answers {
+ val callback = secondArg>()
+ callback.accept(descriptor)
+ }
+ val result = geo.getMapStyleDescriptor()
+ assertSame(descriptor, result)
+ }
+
+ @Test
+ fun `returns map style descriptor with options`() = runBlocking {
+ val descriptor = MapStyleDescriptor("")
+ val options = GetMapStyleDescriptorOptions.builder().mapName("map").build()
+ every { delegate.getMapStyleDescriptor(options, any(), any()) } answers {
+ val callback = secondArg>()
+ callback.accept(descriptor)
+ }
+ val result = geo.getMapStyleDescriptor(options)
+ assertSame(descriptor, result)
+ }
+
+ @Test(expected = GeoException::class)
+ fun `throws map style descriptor error`(): Unit = runBlocking {
+ val error = GeoException("message", "suggestion")
+ every { delegate.getMapStyleDescriptor(any(), any(), any()) } answers {
+ val callback = lastArg>()
+ callback.accept(error)
+ }
+ geo.getMapStyleDescriptor()
+ }
+
+ @Test
+ fun `returns search by text result`() = runBlocking {
+ val query = "query"
+ val searchResult = GeoSearchResult.withPlaces(emptyList())
+ every { delegate.searchByText(query, any(), any(), any()) } answers {
+ val callback = thirdArg>()
+ callback.accept(searchResult)
+ }
+ val result = geo.searchByText(query)
+ assertSame(searchResult, result)
+ }
+
+ @Test
+ fun `returns search by text result with options`() = runBlocking {
+ val query = "query"
+ val options = GeoSearchByTextOptions.builder().maxResults(4).build()
+ val searchResult = GeoSearchResult.withPlaces(emptyList())
+ every { delegate.searchByText(query, options, any(), any()) } answers {
+ val callback = thirdArg>()
+ callback.accept(searchResult)
+ }
+ val result = geo.searchByText(query, options)
+ assertSame(searchResult, result)
+ }
+
+ @Test(expected = GeoException::class)
+ fun `throws search by text error`(): Unit = runBlocking {
+ val query = "query"
+ val error = GeoException("message", "suggestion")
+ every { delegate.searchByText(query, any(), any(), any()) } answers {
+ val callback = lastArg>()
+ callback.accept(error)
+ }
+ geo.searchByText(query)
+ }
+
+ @Test
+ fun `returns search by coordinates result`() = runBlocking {
+ val position = Coordinates()
+ val searchResult = GeoSearchResult.withPlaces(emptyList())
+ every { delegate.searchByCoordinates(position, any(), any(), any()) } answers {
+ val callback = thirdArg>()
+ callback.accept(searchResult)
+ }
+ val result = geo.searchByCoordinates(position)
+ assertSame(searchResult, result)
+ }
+
+ @Test
+ fun `returns search by coordinates result with options`() = runBlocking {
+ val position = Coordinates()
+ val options = GeoSearchByCoordinatesOptions.builder().maxResults(3).build()
+ val searchResult = GeoSearchResult.withPlaces(emptyList())
+ every { delegate.searchByCoordinates(position, options, any(), any()) } answers {
+ val callback = thirdArg>()
+ callback.accept(searchResult)
+ }
+ val result = geo.searchByCoordinates(position, options)
+ assertSame(searchResult, result)
+ }
+
+ @Test(expected = GeoException::class)
+ fun `throws search by coordinates error`(): Unit = runBlocking {
+ val position = Coordinates()
+ val error = GeoException("message", "suggestion")
+ every { delegate.searchByCoordinates(position, any(), any(), any()) } answers {
+ val callback = lastArg>()
+ callback.accept(error)
+ }
+ geo.searchByCoordinates(position)
+ }
+}
diff --git a/core-kotlin/src/test/java/com/amplifyframework/kotlin/storage/KotlinStorageFacadeTest.kt b/core-kotlin/src/test/java/com/amplifyframework/kotlin/storage/KotlinStorageFacadeTest.kt
index 0542a5504c..f2df40d4de 100644
--- a/core-kotlin/src/test/java/com/amplifyframework/kotlin/storage/KotlinStorageFacadeTest.kt
+++ b/core-kotlin/src/test/java/com/amplifyframework/kotlin/storage/KotlinStorageFacadeTest.kt
@@ -33,6 +33,7 @@ import com.amplifyframework.storage.result.StorageUploadFileResult
import com.amplifyframework.storage.result.StorageUploadInputStreamResult
import io.mockk.every
import io.mockk.mockk
+import io.mockk.verifyOrder
import java.io.File
import java.io.InputStream
import java.net.URL
@@ -102,12 +103,13 @@ class KotlinStorageFacadeTest {
fun downloadFileSucceeds(): Unit = runBlocking {
val fromRemoteKey = "kool-pic.png"
val toLocalFile = File("/local/path/kool-pic.png")
-
+ val transferId = UUID.randomUUID().toString()
val progressEvents = (0L until 101 step 50)
.map { amount -> StorageTransferProgress(amount, 100) }
val cancelable = mockk>()
every { cancelable.cancel() } answers {}
+ every { cancelable.transferId } answers { transferId }
every {
delegate.downloadFile(eq(fromRemoteKey), eq(toLocalFile), any(), any(), any(), any())
@@ -127,11 +129,45 @@ class KotlinStorageFacadeTest {
}
val download = storage.downloadFile(fromRemoteKey, toLocalFile)
+ assertEquals(transferId, download.transferId)
val actualProgressEvents = download.progress().take(progressEvents.size).toList()
assertEquals(progressEvents, actualProgressEvents)
assertEquals(toLocalFile, download.result().file)
}
+ /**
+ * When the downloadFile() kotlin operation invokes pause, resume & cancel operation then corresponding
+ * delegate apis are invoked.
+ */
+ @Test
+ fun performActionsOnDownloadFile(): Unit = runBlocking {
+ val fromRemoteKey = "kool-pic.png"
+ val toLocalFile = File("/local/path/kool-pic.png")
+ val transferId = UUID.randomUUID().toString()
+
+ val cancelable = mockk>()
+ every { cancelable.cancel() } answers {}
+ every { cancelable.pause() } answers {}
+ every { cancelable.resume() } answers {}
+ every { cancelable.transferId } answers { transferId }
+
+ every {
+ delegate.downloadFile(eq(fromRemoteKey), eq(toLocalFile), any(), any(), any(), any())
+ } answers {
+ cancelable
+ }
+
+ val download = storage.downloadFile(fromRemoteKey, toLocalFile)
+ download.pause()
+ download.resume()
+ download.cancel()
+ verifyOrder {
+ cancelable.pause()
+ cancelable.resume()
+ cancelable.cancel()
+ }
+ }
+
/**
* When the downloadFile() API emits an error, it should be thrown from
* the coroutine API.
@@ -141,10 +177,10 @@ class KotlinStorageFacadeTest {
val fromRemoteKey = "kool-pic.png"
val toLocalFile = File("/local/path/kool-pic.png")
val error = StorageException("uh", "oh")
-
+ val transferId = UUID.randomUUID().toString()
val cancelable = mockk>()
every { cancelable.cancel() } answers {}
-
+ every { cancelable.transferId } answers { transferId }
every {
delegate.downloadFile(eq(fromRemoteKey), eq(toLocalFile), any(), any(), any(), any())
} answers {
@@ -169,10 +205,10 @@ class KotlinStorageFacadeTest {
val progressEvents = (0L until 101 step 50)
.map { amount -> StorageTransferProgress(amount, 100) }
-
+ val transferId = UUID.randomUUID().toString()
val cancelable = mockk>()
every { cancelable.cancel() } answers {}
-
+ every { cancelable.transferId } answers { transferId }
every {
delegate.uploadFile(eq(toRemoteKey), eq(fromLocalFile), any(), any(), any(), any())
} answers {
@@ -191,7 +227,9 @@ class KotlinStorageFacadeTest {
}
val upload = storage.uploadFile(toRemoteKey, fromLocalFile)
+ assertEquals(transferId, upload.transferId)
val receivedProgressEvents = upload.progress().take(3).toList()
+ assertEquals(transferId, upload.transferId)
assertEquals(progressEvents, receivedProgressEvents)
assertEquals(toRemoteKey, upload.result().key)
}
@@ -205,10 +243,10 @@ class KotlinStorageFacadeTest {
val toRemoteKey = "kool-pic.png"
val fromLocalFile = File("/local/path/kool-pic.png")
val error = StorageException("uh", "oh")
-
+ val transferId = UUID.randomUUID().toString()
val cancelable = mockk>()
every { cancelable.cancel() } answers {}
-
+ every { cancelable.transferId } answers { transferId }
every {
delegate.uploadFile(eq(toRemoteKey), eq(fromLocalFile), any(), any(), any(), any())
} answers {
@@ -222,6 +260,37 @@ class KotlinStorageFacadeTest {
.result()
}
+ /**
+ * When the uploadFile() kotlin operation invokes pause, resume & cancel operation then corresponding
+ * delegate apis are invoked.
+ */
+ @Test
+ fun performActionOnUploadFileSucceeds() = runBlocking {
+ val toRemoteKey = "kool-pic.png"
+ val fromLocalFile = File("/local/path/kool-pic.png")
+ val transferId = UUID.randomUUID().toString()
+ val cancelable = mockk>()
+ every { cancelable.cancel() } answers {}
+ every { cancelable.pause() } answers {}
+ every { cancelable.resume() } answers {}
+ every { cancelable.transferId } answers { transferId }
+ every {
+ delegate.uploadFile(eq(toRemoteKey), eq(fromLocalFile), any(), any(), any(), any())
+ } answers {
+ cancelable
+ }
+
+ val upload = storage.uploadFile(toRemoteKey, fromLocalFile)
+ upload.pause()
+ upload.resume()
+ upload.cancel()
+ verifyOrder {
+ cancelable.pause()
+ cancelable.resume()
+ cancelable.cancel()
+ }
+ }
+
/**
* When the underlying uploadInputStream() delegate emits a result,
* it should be returned from the coroutine API.
@@ -230,13 +299,13 @@ class KotlinStorageFacadeTest {
fun uploadInputStreamSucceeds() = runBlocking {
val toRemoteKey = "kool-pic.png"
val fromStream = mockk()
-
+ val transferId = UUID.randomUUID().toString()
val progressEvents = (0L until 101 step 50)
.map { amount -> StorageTransferProgress(amount, 100) }
val cancelable = mockk>()
every { cancelable.cancel() } answers {}
-
+ every { cancelable.transferId } answers { transferId }
every {
delegate.uploadInputStream(eq(toRemoteKey), eq(fromStream), any(), any(), any(), any())
} answers {
@@ -255,6 +324,7 @@ class KotlinStorageFacadeTest {
}
val upload = storage.uploadInputStream(toRemoteKey, fromStream)
+ assertEquals(transferId, upload.transferId)
val receivedProgressEvents = upload.progress().take(3).toList()
assertEquals(progressEvents, receivedProgressEvents)
assertEquals(toRemoteKey, upload.result().key)
@@ -269,9 +339,10 @@ class KotlinStorageFacadeTest {
val toRemoteKey = "kool-pic.png"
val fromStream = mockk()
val error = StorageException("uh", "oh")
-
+ val transferId = UUID.randomUUID().toString()
val cancelable = mockk>()
every { cancelable.cancel() } answers {}
+ every { cancelable.transferId } answers { transferId }
every {
delegate.uploadInputStream(eq(toRemoteKey), eq(fromStream), any(), any(), any(), any())
@@ -286,6 +357,38 @@ class KotlinStorageFacadeTest {
.result()
}
+ /**
+ * When the underlying uploadInputStream() kotlin operation invokes pause, resume & cancel operation then the
+ * corresponding delegate apis are invoked.
+ */
+ @Test
+ fun performActionOnUploadInputStream() = runBlocking {
+ val toRemoteKey = "kool-pic.png"
+ val fromStream = mockk()
+ val transferId = UUID.randomUUID().toString()
+ val cancelable = mockk>()
+ every { cancelable.cancel() } answers {}
+ every { cancelable.pause() } answers {}
+ every { cancelable.resume() } answers {}
+ every { cancelable.transferId } answers { transferId }
+
+ every {
+ delegate.uploadInputStream(eq(toRemoteKey), eq(fromStream), any(), any(), any(), any())
+ } answers {
+ cancelable
+ }
+
+ val upload = storage.uploadInputStream(toRemoteKey, fromStream)
+ upload.pause()
+ upload.resume()
+ upload.cancel()
+ verifyOrder {
+ cancelable.pause()
+ cancelable.resume()
+ cancelable.cancel()
+ }
+ }
+
/**
* When the remove() delegate emits a result, it should be returned
* from the coroutine API.
diff --git a/maplibre-adapter/build.gradle b/maplibre-adapter/build.gradle
index 94709f15f6..6eb70dcc51 100644
--- a/maplibre-adapter/build.gradle
+++ b/maplibre-adapter/build.gradle
@@ -32,6 +32,7 @@ android {
}
dependencies {
+ androidTestImplementation project(path: ':maplibre-adapter')
def lifecycleVersion = "2.4.1"
implementation project(":aws-auth-cognito")
diff --git a/maplibre-adapter/src/androidTest/java/com/amplifyframework/geo/maplibre/MapViewStressTest.kt b/maplibre-adapter/src/androidTest/java/com/amplifyframework/geo/maplibre/MapViewStressTest.kt
new file mode 100644
index 0000000000..7b62a93f56
--- /dev/null
+++ b/maplibre-adapter/src/androidTest/java/com/amplifyframework/geo/maplibre/MapViewStressTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2022 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.geo.maplibre
+
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import com.amplifyframework.geo.GeoCategory
+import com.amplifyframework.geo.location.AWSLocationGeoPlugin
+import com.amplifyframework.testutils.sync.SynchronousGeo
+import com.amplifyframework.testutils.sync.TestCategory
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class MapViewStressTest {
+ @get:Rule
+ var rule = ActivityScenarioRule(MapViewTestActivity::class.java)
+ private var geo: SynchronousGeo? = null
+
+ /**
+ * Set up test categories to be used for testing.
+ */
+ @Before
+ fun setUpBeforeTest() {
+ val geoPlugin = AWSLocationGeoPlugin()
+ val geoCategory = TestCategory.forPlugin(geoPlugin) as GeoCategory
+ geo = SynchronousGeo.delegatingTo(geoCategory)
+ }
+
+ /**
+ * Calls mapView.setStyle 50 times
+ */
+ @Test
+ fun testMultipleSetStyle() = runBlockingSignedIn(rule) {
+ repeat(50) {
+ val mapStyle = suspendCoroutine { continuation ->
+ rule.scenario.onActivity { activity ->
+ activity.mapView.addOnDidFailLoadingMapListener { error ->
+ continuation.resumeWithException(RuntimeException(error))
+ }
+ activity.mapView.setStyle { style ->
+ continuation.resume(style)
+ }
+ }
+ }
+ Assert.assertNotNull(mapStyle)
+ }
+ }
+
+ private fun runBlockingSignedIn(
+ rule: ActivityScenarioRule,
+ block: suspend CoroutineScope.() -> T
+ ): T {
+ return runBlocking(Dispatchers.Main) {
+ rule.scenario.onActivity {
+ signOutFromCognito() // first sign out to ensure we are in clean state
+ signInWithCognito()
+ }
+ val result = block()
+ rule.scenario.onActivity { signOutFromCognito() }
+ result
+ }
+ }
+
+ private fun signInWithCognito() {
+ val (username, password) = Credentials.load(ApplicationProvider.getApplicationContext())
+ val result = AmplifyWrapper.auth.signIn(username, password)
+ println("SignIn complete: ${result.isSignedIn}")
+ }
+
+ private fun signOutFromCognito() {
+ AmplifyWrapper.auth.signOut()
+ }
+}
diff --git a/rxbindings/build.gradle b/rxbindings/build.gradle
index 78061b9c03..72ffa620c3 100644
--- a/rxbindings/build.gradle
+++ b/rxbindings/build.gradle
@@ -13,7 +13,11 @@
* permissions and limitations under the License.
*/
-apply plugin: 'com.android.library'
+plugins {
+ id "com.android.library"
+ id "kotlin-android"
+}
+
apply from: rootProject.file("configuration/checkstyle.gradle")
apply from: rootProject.file("configuration/publishing.gradle")
@@ -26,6 +30,7 @@ dependencies {
testImplementation project(path: ':testutils')
testImplementation dependency.junit
testImplementation dependency.mockito
+ testImplementation dependency.mockk
testImplementation dependency.androidx.test.core
testImplementation dependency.robolectric
testImplementation project(path: ':rxbindings')
diff --git a/rxbindings/src/main/java/com/amplifyframework/rx/RxAmplify.java b/rxbindings/src/main/java/com/amplifyframework/rx/RxAmplify.java
index b4f144f81c..1216307aa3 100644
--- a/rxbindings/src/main/java/com/amplifyframework/rx/RxAmplify.java
+++ b/rxbindings/src/main/java/com/amplifyframework/rx/RxAmplify.java
@@ -41,6 +41,7 @@ private RxAmplify() {}
@SuppressWarnings({"checkstyle:all"}) public static final RxHubCategoryBehavior Hub = new RxHubBinding();
@SuppressWarnings({"checkstyle:all"}) public static final RxDataStoreCategoryBehavior DataStore = new RxDataStoreBinding();
@SuppressWarnings({"checkstyle:all"}) public static final RxPredictionsCategoryBehavior Predictions = new RxPredictionsBinding();
+ @SuppressWarnings({"checkstyle:all"}) public static final RxGeoCategoryBehavior Geo = new RxGeoBinding();
/**
* Read the configuration from amplifyconfiguration.json file.
diff --git a/rxbindings/src/main/java/com/amplifyframework/rx/RxGeoBinding.java b/rxbindings/src/main/java/com/amplifyframework/rx/RxGeoBinding.java
new file mode 100644
index 0000000000..3042ed753a
--- /dev/null
+++ b/rxbindings/src/main/java/com/amplifyframework/rx/RxGeoBinding.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2022 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.rx;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.amplifyframework.core.Amplify;
+import com.amplifyframework.geo.GeoCategoryBehavior;
+import com.amplifyframework.geo.GeoException;
+import com.amplifyframework.geo.models.Coordinates;
+import com.amplifyframework.geo.models.MapStyle;
+import com.amplifyframework.geo.models.MapStyleDescriptor;
+import com.amplifyframework.geo.options.GeoSearchByCoordinatesOptions;
+import com.amplifyframework.geo.options.GeoSearchByTextOptions;
+import com.amplifyframework.geo.options.GetMapStyleDescriptorOptions;
+import com.amplifyframework.geo.result.GeoSearchResult;
+
+import java.util.Collection;
+import java.util.Objects;
+
+import io.reactivex.rxjava3.core.Single;
+
+final class RxGeoBinding implements RxGeoCategoryBehavior {
+ private final GeoCategoryBehavior delegate;
+
+ RxGeoBinding() {
+ delegate = Amplify.Geo;
+ }
+
+ @VisibleForTesting
+ RxGeoBinding(@NonNull GeoCategoryBehavior delegate) {
+ this.delegate = Objects.requireNonNull(delegate);
+ }
+
+ @Override
+ public Single> getAvailableMaps() {
+ return toSingle(delegate::getAvailableMaps);
+ }
+
+ @Override
+ public Single getDefaultMap() {
+ return toSingle(delegate::getDefaultMap);
+ }
+
+ @Override
+ public Single getMapStyleDescriptor() {
+ return toSingle(delegate::getMapStyleDescriptor);
+ }
+
+ @Override
+ public Single getMapStyleDescriptor(@NonNull GetMapStyleDescriptorOptions options) {
+ return toSingle((onResult, onError) -> delegate.getMapStyleDescriptor(options, onResult, onError));
+ }
+
+ @Override
+ public Single searchByText(@NonNull String query) {
+ return toSingle((onResult, onError) -> delegate.searchByText(query, onResult, onError));
+ }
+
+ @Override
+ public Single searchByText(@NonNull String query, @NonNull GeoSearchByTextOptions options) {
+ return toSingle((onResult, onError) -> delegate.searchByText(query, options, onResult, onError));
+ }
+
+ @Override
+ public Single searchByCoordinates(@NonNull Coordinates position) {
+ return toSingle((onResult, onError) -> delegate.searchByCoordinates(position, onResult, onError));
+ }
+
+ @Override
+ public Single searchByCoordinates(
+ @NonNull Coordinates position,
+ @NonNull GeoSearchByCoordinatesOptions options
+ ) {
+ return toSingle((onResult, onError) -> delegate.searchByCoordinates(position, options, onResult, onError));
+ }
+
+ private static Single toSingle(RxAdapters.VoidBehaviors.ResultEmitter behavior) {
+ return RxAdapters.VoidBehaviors.toSingle(behavior);
+ }
+}
diff --git a/rxbindings/src/main/java/com/amplifyframework/rx/RxGeoCategoryBehavior.java b/rxbindings/src/main/java/com/amplifyframework/rx/RxGeoCategoryBehavior.java
new file mode 100644
index 0000000000..35d580aa2c
--- /dev/null
+++ b/rxbindings/src/main/java/com/amplifyframework/rx/RxGeoCategoryBehavior.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2022 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.rx;
+
+import androidx.annotation.NonNull;
+
+import com.amplifyframework.geo.GeoCategoryBehavior;
+import com.amplifyframework.geo.GeoException;
+import com.amplifyframework.geo.models.Coordinates;
+import com.amplifyframework.geo.models.MapStyle;
+import com.amplifyframework.geo.models.MapStyleDescriptor;
+import com.amplifyframework.geo.options.GeoSearchByCoordinatesOptions;
+import com.amplifyframework.geo.options.GeoSearchByTextOptions;
+import com.amplifyframework.geo.options.GetMapStyleDescriptorOptions;
+import com.amplifyframework.geo.result.GeoSearchResult;
+
+import java.util.Collection;
+
+import io.reactivex.rxjava3.core.Single;
+
+/**
+ * An Rx-idiomatic expression of the behaviors in {@link GeoCategoryBehavior}.
+ */
+public interface RxGeoCategoryBehavior {
+ /**
+ * Gets a collection of maps and their corresponding styles.
+ *
+ * @return An Rx {@link Single} which emits a {@link MapStyle} on success, or a
+ * {@link GeoException} on failure
+ */
+ Single> getAvailableMaps();
+
+ /**
+ * Gets the default map and style from available maps.
+ *
+ * @return An Rx {@link Single} which emits a {@link MapStyle} on success, or a
+ * {@link GeoException} on failure
+ */
+ Single getDefaultMap();
+
+ /**
+ * Uses default options to get map style descriptor JSON.
+ *
+ * @return An Rx {@link Single} which emits a {@link MapStyleDescriptor} on success, or a
+ * {@link GeoException} on failure
+ */
+ Single getMapStyleDescriptor();
+
+ /**
+ * Uses given options to get map style descriptor JSON.
+ *
+ * @param options Options to specify for this operation.
+ * @return An Rx {@link Single} which emits a {@link MapStyleDescriptor} on success, or a
+ * {@link GeoException} on failure
+ */
+ Single getMapStyleDescriptor(@NonNull GetMapStyleDescriptorOptions options);
+
+ /**
+ * Searches for locations that match text query.
+ *
+ * @param query Search query text.
+ * @return An Rx {@link Single} which emits a {@link GeoSearchResult} on success, or a
+ * {@link GeoException} on failure
+ */
+ Single searchByText(@NonNull String query);
+
+ /**
+ * Searches for locations that match text query.
+ *
+ * @param query Search query text.
+ * @param options Search options to use.
+ * @return An Rx {@link Single} which emits a {@link GeoSearchResult} on success, or a
+ * {@link GeoException} on failure
+ */
+ Single searchByText(
+ @NonNull String query,
+ @NonNull GeoSearchByTextOptions options
+ );
+
+ /**
+ * Searches for location with given set of coordinates.
+ *
+ * @param position Coordinates to look-up.
+ * @return An Rx {@link Single} which emits a {@link GeoSearchResult} on success, or a
+ * {@link GeoException} on failure
+ */
+ Single searchByCoordinates(@NonNull Coordinates position);
+
+ /**
+ * Searches for location with given set of coordinates.
+ *
+ * @param position Coordinates to look-up.
+ * @param options Search options to use.
+ * @return An Rx {@link Single} which emits a {@link GeoSearchResult} on success, or a
+ * {@link GeoException} on failure
+ */
+ Single searchByCoordinates(
+ @NonNull Coordinates position,
+ @NonNull GeoSearchByCoordinatesOptions options
+ );
+}
diff --git a/rxbindings/src/main/java/com/amplifyframework/rx/RxStorageBinding.java b/rxbindings/src/main/java/com/amplifyframework/rx/RxStorageBinding.java
index 7ab4ddc9a3..4a89d5e3bc 100644
--- a/rxbindings/src/main/java/com/amplifyframework/rx/RxStorageBinding.java
+++ b/rxbindings/src/main/java/com/amplifyframework/rx/RxStorageBinding.java
@@ -20,8 +20,8 @@
import com.amplifyframework.core.Amplify;
import com.amplifyframework.core.Consumer;
-import com.amplifyframework.core.async.Cancelable;
import com.amplifyframework.core.async.NoOpCancelable;
+import com.amplifyframework.core.async.Resumable;
import com.amplifyframework.rx.RxAdapters.CancelableBehaviors;
import com.amplifyframework.storage.StorageCategory;
import com.amplifyframework.storage.StorageCategoryBehavior;
@@ -186,10 +186,10 @@ private Single toSingle(CancelableBehaviors.ResultEmitter The type that represents the result of a given operation.
*/
- public static final class RxProgressAwareSingleOperation implements RxAdapters.RxSingleOperation {
+ public static final class RxProgressAwareSingleOperation implements RxAdapters.RxSingleOperation, Resumable {
private final PublishSubject progressSubject;
private final ReplaySubject resultSubject;
- private final Cancelable amplifyOperation;
+ private final StorageTransferOperation, ?> amplifyOperation;
RxProgressAwareSingleOperation(RxStorageTransferCallbackMapper callbacks) {
progressSubject = PublishSubject.create();
@@ -199,6 +199,26 @@ public static final class RxProgressAwareSingleOperation implements RxAdapter
resultSubject::onError);
}
+ /**
+ * Return the transfer ID for this operation.
+ *
+ * @return unique transferId for this operation
+ */
+ @NonNull
+ public String getTransferId() {
+ return amplifyOperation.getTransferId();
+ }
+
+ @Override
+ public void resume() {
+ amplifyOperation.resume();
+ }
+
+ @Override
+ public void pause() {
+ amplifyOperation.pause();
+ }
+
@Override
public void cancel() {
amplifyOperation.cancel();
@@ -235,7 +255,7 @@ public Observable observeProgress() {
* @param The type that represents the result of a given operation.
*/
interface RxStorageTransferCallbackMapper {
- Cancelable emitTo(
+ StorageTransferOperation, ?> emitTo(
Consumer onProgress,
Consumer onItem,
Consumer onError
diff --git a/rxbindings/src/test/java/com/amplifyframework/rx/RxGeoBindingTest.kt b/rxbindings/src/test/java/com/amplifyframework/rx/RxGeoBindingTest.kt
new file mode 100644
index 0000000000..ce95160611
--- /dev/null
+++ b/rxbindings/src/test/java/com/amplifyframework/rx/RxGeoBindingTest.kt
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2022 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.rx
+
+import com.amplifyframework.core.Consumer
+import com.amplifyframework.geo.GeoCategoryBehavior
+import com.amplifyframework.geo.GeoException
+import com.amplifyframework.geo.models.Coordinates
+import com.amplifyframework.geo.models.MapStyle
+import com.amplifyframework.geo.models.MapStyleDescriptor
+import com.amplifyframework.geo.options.GeoSearchByCoordinatesOptions
+import com.amplifyframework.geo.options.GeoSearchByTextOptions
+import com.amplifyframework.geo.options.GetMapStyleDescriptorOptions
+import com.amplifyframework.geo.result.GeoSearchResult
+import io.mockk.every
+import io.mockk.mockk
+import java.util.concurrent.TimeUnit
+import org.junit.Test
+
+private const val TIMEOUT_SECONDS = 2L
+
+/**
+ * Unit tests for the [RxGeoBinding] class
+ */
+class RxGeoBindingTest {
+ private val delegate = mockk()
+ private val geo = RxGeoBinding(delegate)
+
+ @Test
+ fun `returns available maps`() {
+ val maps = listOf(MapStyle("a", "b"), MapStyle("c", "d"))
+ every { delegate.getAvailableMaps(any(), any()) } answers {
+ val callback = firstArg>>()
+ callback.accept(maps)
+ }
+
+ val observer = geo.availableMaps.test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoErrors().assertValue(maps)
+ }
+
+ @Test
+ fun `returns error for available maps`() {
+ val error = GeoException("message", "suggestion")
+ every { delegate.getAvailableMaps(any(), any()) } answers {
+ val callback = lastArg>()
+ callback.accept(error)
+ }
+
+ val observer = geo.availableMaps.test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoValues().assertError(error)
+ }
+
+ @Test
+ fun `returns default map`() {
+ val map = MapStyle("map", "style")
+ every { delegate.getDefaultMap(any(), any()) } answers {
+ val callback = firstArg>()
+ callback.accept(map)
+ }
+
+ val observer = geo.defaultMap.test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoErrors().assertValue(map)
+ }
+
+ @Test
+ fun `returns error for default map`() {
+ val error = GeoException("message", "suggestion")
+ every { delegate.getDefaultMap(any(), any()) } answers {
+ val callback = lastArg>()
+ callback.accept(error)
+ }
+
+ val observer = geo.defaultMap.test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoValues().assertError(error)
+ }
+
+ @Test
+ fun `returns map style descriptor`() {
+ val descriptor = MapStyleDescriptor("")
+ every { delegate.getMapStyleDescriptor(any(), any()) } answers {
+ val callback = firstArg>()
+ callback.accept(descriptor)
+ }
+
+ val observer = geo.mapStyleDescriptor.test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoErrors().assertValue(descriptor)
+ }
+
+ @Test
+ fun `returns map style descriptor with options`() {
+ val options = GetMapStyleDescriptorOptions.builder().mapName("map").build()
+ val descriptor = MapStyleDescriptor("")
+ every { delegate.getMapStyleDescriptor(options, any(), any()) } answers {
+ val callback = secondArg>()
+ callback.accept(descriptor)
+ }
+
+ val observer = geo.getMapStyleDescriptor(options).test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoErrors().assertValue(descriptor)
+ }
+
+ @Test
+ fun `returns error for map style descriptor`() {
+ val error = GeoException("message", "suggestion")
+ every { delegate.getMapStyleDescriptor(any(), any()) } answers {
+ val callback = lastArg>()
+ callback.accept(error)
+ }
+
+ val observer = geo.mapStyleDescriptor.test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoValues().assertError(error)
+ }
+
+ @Test
+ fun `returns error for map style descriptor with options`() {
+ val error = GeoException("message", "suggestion")
+ val options = GetMapStyleDescriptorOptions.builder().mapName("map").build()
+ every { delegate.getMapStyleDescriptor(options, any(), any()) } answers {
+ val callback = lastArg>()
+ callback.accept(error)
+ }
+
+ val observer = geo.getMapStyleDescriptor(options).test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoValues().assertError(error)
+ }
+
+ @Test
+ fun `returns search by text`() {
+ val query = "query"
+ val searchResult = GeoSearchResult.withPlaces(emptyList())
+ every { delegate.searchByText(query, any(), any()) } answers {
+ val callback = secondArg>()
+ callback.accept(searchResult)
+ }
+
+ val observer = geo.searchByText(query).test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoErrors().assertValue(searchResult)
+ }
+
+ @Test
+ fun `returns search by text with options`() {
+ val query = "query"
+ val options = GeoSearchByTextOptions.builder().maxResults(2).build()
+ val searchResult = GeoSearchResult.withPlaces(emptyList())
+ every { delegate.searchByText(query, options, any(), any()) } answers {
+ val callback = thirdArg>()
+ callback.accept(searchResult)
+ }
+
+ val observer = geo.searchByText(query, options).test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoErrors().assertValue(searchResult)
+ }
+
+ @Test
+ fun `returns error for search by text`() {
+ val query = "query"
+ val error = GeoException("message", "suggestion")
+ every { delegate.searchByText(query, any(), any()) } answers {
+ val callback = lastArg>()
+ callback.accept(error)
+ }
+
+ val observer = geo.searchByText(query).test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoValues().assertError(error)
+ }
+
+ @Test
+ fun `returns error for search by text with options`() {
+ val query = "query"
+ val options = GeoSearchByTextOptions.builder().maxResults(5).build()
+ val error = GeoException("message", "suggestion")
+ every { delegate.searchByText(query, options, any(), any()) } answers {
+ val callback = lastArg>()
+ callback.accept(error)
+ }
+
+ val observer = geo.searchByText(query, options).test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoValues().assertError(error)
+ }
+
+ @Test
+ fun `returns search by coordinates`() {
+ val coordinates = Coordinates(2.0, 5.0)
+ val searchResult = GeoSearchResult.withPlaces(emptyList())
+ every { delegate.searchByCoordinates(coordinates, any(), any()) } answers {
+ val callback = secondArg>()
+ callback.accept(searchResult)
+ }
+
+ val observer = geo.searchByCoordinates(coordinates).test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoErrors().assertValue(searchResult)
+ }
+
+ @Test
+ fun `returns search by coordinates with options`() {
+ val coordinates = Coordinates(2.0, 5.0)
+ val options = GeoSearchByCoordinatesOptions.builder().maxResults(6).build()
+ val searchResult = GeoSearchResult.withPlaces(emptyList())
+ every { delegate.searchByCoordinates(coordinates, options, any(), any()) } answers {
+ val callback = thirdArg>()
+ callback.accept(searchResult)
+ }
+
+ val observer = geo.searchByCoordinates(coordinates, options).test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoErrors().assertValue(searchResult)
+ }
+
+ @Test
+ fun `returns error for search by coordinates`() {
+ val coordinates = Coordinates(3.0, 4.0)
+ val error = GeoException("message", "suggestion")
+ every { delegate.searchByCoordinates(coordinates, any(), any()) } answers {
+ val callback = lastArg>()
+ callback.accept(error)
+ }
+
+ val observer = geo.searchByCoordinates(coordinates).test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoValues().assertError(error)
+ }
+
+ @Test
+ fun `returns error for search by coordinates with options`() {
+ val coordinates = Coordinates(3.0, 4.0)
+ val options = GeoSearchByCoordinatesOptions.builder().maxResults(6).build()
+ val error = GeoException("message", "suggestion")
+ every { delegate.searchByCoordinates(coordinates, options, any(), any()) } answers {
+ val callback = lastArg>()
+ callback.accept(error)
+ }
+
+ val observer = geo.searchByCoordinates(coordinates, options).test()
+ observer.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+
+ observer.assertNoValues().assertError(error)
+ }
+}
diff --git a/rxbindings/src/test/java/com/amplifyframework/rx/RxStorageBindingTest.java b/rxbindings/src/test/java/com/amplifyframework/rx/RxStorageBindingTest.java
index 02672be308..0980fb9038 100644
--- a/rxbindings/src/test/java/com/amplifyframework/rx/RxStorageBindingTest.java
+++ b/rxbindings/src/test/java/com/amplifyframework/rx/RxStorageBindingTest.java
@@ -49,6 +49,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.InOrder;
import org.robolectric.RobolectricTestRunner;
import java.io.ByteArrayInputStream;
@@ -58,6 +59,7 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
+import java.util.UUID;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.core.Observable;
@@ -65,9 +67,11 @@
import io.reactivex.rxjava3.observers.TestObserver;
import static com.amplifyframework.rx.Matchers.anyConsumer;
+import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -202,6 +206,42 @@ public void downloadFileReturnsResult() throws InterruptedException {
testProgressObserver.assertValueCount(5);
}
+ /**
+ * When {@link StorageCategoryBehavior#downloadFile(String, File, StorageDownloadFileOptions,
+ * Consumer, Consumer, Consumer)} invokes its pause, resume and cancel operation, the {@link
+ * StorageDownloadFileOperation}
+ * should invoke corresponding api.
+ *
+ * @throws InterruptedException not expected.
+ */
+ @Test
+ public void performActionOnDownloadFile() throws InterruptedException {
+ StorageDownloadFileResult result = StorageDownloadFileResult.fromFile(mock(File.class));
+ String transferId = UUID.randomUUID().toString();
+ StorageDownloadFileOperation> storageDownloadFileOperationMock = mock(StorageDownloadFileOperation.class);
+ when(storageDownloadFileOperationMock.getTransferId()).thenReturn(transferId);
+ doAnswer(invocation -> {
+ return storageDownloadFileOperationMock;
+ }).when(delegate).downloadFile(eq(remoteKey),
+ eq(localFile),
+ any(StorageDownloadFileOptions.class),
+ anyConsumer(),
+ anyConsumer(),
+ anyConsumer());
+
+ RxStorageBinding.RxProgressAwareSingleOperation rxOperation =
+ rxStorage.downloadFile(remoteKey, localFile, StorageDownloadFileOptions.defaultInstance());
+ assertEquals(transferId, rxOperation.getTransferId());
+ InOrder inOrder = inOrder(storageDownloadFileOperationMock);
+
+ rxOperation.pause();
+ rxOperation.resume();
+ rxOperation.cancel();
+ inOrder.verify(storageDownloadFileOperationMock).pause();
+ inOrder.verify(storageDownloadFileOperationMock).resume();
+ inOrder.verify(storageDownloadFileOperationMock).cancel();
+ }
+
/**
* When {@link StorageCategoryBehavior#downloadFile(String, File, Consumer, Consumer)} invokes
* its error callback, the {@link StorageException} is communicated via the {@link Single}
@@ -268,6 +308,40 @@ public void uploadFileReturnsResult() throws InterruptedException {
testProgressObserver.assertValueCount(5);
}
+ /**
+ * When {@link StorageCategoryBehavior#uploadFile(String, File, Consumer, Consumer)} returns
+ * a {@link RxStorageBinding.RxProgressAwareSingleOperation}, then the pause, resume and cancel action
+ * performed on the rxOperation should invoke corresponding api in {@link StorageUploadFileOperation}.
+ *
+ * @throws InterruptedException Not expected.
+ */
+ @Test
+ public void performActionOnUploadFile() throws InterruptedException {
+ StorageUploadFileResult result = StorageUploadFileResult.fromKey(remoteKey);
+ String transferId = UUID.randomUUID().toString();
+ StorageUploadFileOperation> storageUploadFileOperationMock = mock(StorageUploadFileOperation.class);
+ when(storageUploadFileOperationMock.getTransferId()).thenReturn(transferId);
+ doAnswer(invocation -> {
+ return storageUploadFileOperationMock;
+ }).when(delegate).uploadFile(eq(remoteKey),
+ eq(localFile),
+ any(StorageUploadFileOptions.class),
+ anyConsumer(),
+ anyConsumer(),
+ anyConsumer());
+
+ RxStorageBinding.RxProgressAwareSingleOperation rxOperation =
+ rxStorage.uploadFile(remoteKey, localFile);
+ assertEquals(transferId, rxOperation.getTransferId());
+ InOrder inOrder = inOrder(storageUploadFileOperationMock);
+ rxOperation.pause();
+ rxOperation.resume();
+ rxOperation.cancel();
+ inOrder.verify(storageUploadFileOperationMock).pause();
+ inOrder.verify(storageUploadFileOperationMock).resume();
+ inOrder.verify(storageUploadFileOperationMock).cancel();
+ }
+
/**
* When {@link StorageCategoryBehavior#uploadInputStream(String, InputStream, Consumer, Consumer)} returns
* a {@link StorageUploadInputStreamResult}, then the {@link Single} returned by
@@ -307,6 +381,42 @@ public void uploadInputStreamReturnsResult() throws InterruptedException {
testProgressObserver.assertValueCount(5);
}
+ /**
+ * When {@link StorageCategoryBehavior#uploadInputStream(String, InputStream, Consumer, Consumer)} returns
+ * a {@link StorageUploadInputStreamResult}, then the {@link Single} returned by
+ * {@link RxStorageCategoryBehavior#uploadInputStream(String, InputStream)} should invoke corresponding API in
+ * {@link StorageUploadInputStreamOperation}.
+ *
+ * @throws InterruptedException Not expected.
+ */
+ @Test
+ public void performActionOnUploadInputStream() throws InterruptedException {
+ StorageUploadInputStreamResult result = StorageUploadInputStreamResult.fromKey(remoteKey);
+ String transferId = UUID.randomUUID().toString();
+ StorageUploadInputStreamOperation> storageUploadInputStreamOperationMock =
+ mock(StorageUploadInputStreamOperation.class);
+ when(storageUploadInputStreamOperationMock.getTransferId()).thenReturn(transferId);
+ doAnswer(invocation -> {
+ return storageUploadInputStreamOperationMock;
+ }).when(delegate).uploadInputStream(eq(remoteKey),
+ eq(localInputStream),
+ any(StorageUploadInputStreamOptions.class),
+ anyConsumer(),
+ anyConsumer(),
+ anyConsumer());
+
+ RxStorageBinding.RxProgressAwareSingleOperation rxOperation =
+ rxStorage.uploadInputStream(remoteKey, localInputStream);
+ assertEquals(transferId, rxOperation.getTransferId());
+ InOrder inOrder = inOrder(storageUploadInputStreamOperationMock);
+ rxOperation.pause();
+ rxOperation.resume();
+ rxOperation.cancel();
+ inOrder.verify(storageUploadInputStreamOperationMock).pause();
+ inOrder.verify(storageUploadInputStreamOperationMock).resume();
+ inOrder.verify(storageUploadInputStreamOperationMock).cancel();
+ }
+
/**
* When {@link StorageCategoryBehavior#uploadFile(String, File, Consumer, Consumer)} returns
* an {@link StorageException}, then the {@link Single} returned by
diff --git a/scripts/run_tests_in_devicefarm_pool.sh b/scripts/run_nightly_tests_in_devicefarm_pool.sh
similarity index 100%
rename from scripts/run_tests_in_devicefarm_pool.sh
rename to scripts/run_nightly_tests_in_devicefarm_pool.sh
diff --git a/scripts/run_test_in_devicefarm.sh b/scripts/run_test_in_devicefarm.sh
index 3997ecbff7..ca53f97d0e 100755
--- a/scripts/run_test_in_devicefarm.sh
+++ b/scripts/run_test_in_devicefarm.sh
@@ -1,5 +1,6 @@
#!/bin/bash
project_arn=$DEVICEFARM_PROJECT_ARN
+max_devices=$NUMBER_OF_DEVICES_TO_TEST
module_name=$1
file_name="$module_name-debug-androidTest.apk"
full_path="$module_name/build/outputs/apk/androidTest/debug/$file_name"
@@ -9,6 +10,11 @@ if [[ -z "${project_arn}" ]]; then
exit 1
fi
+if [[ -z "${max_devices}" ]]; then
+ echo "NUMBER_OF_DEVICES_TO_TEST not set. Defaulting to 1."
+ max_devices=1
+fi
+
# Function to setup the app uploads in device farm
function createUpload {
test_type=$1
@@ -110,7 +116,7 @@ run_arn=`aws devicefarm schedule-run --project-arn=$project_arn \
"filters": [
{"attribute": "ARN", "operator":"IN", "values":["'$minDevice'", "'$middleDevice'", "'$latestDevice'"]}
],
- "maxDevices": 3
+ "maxDevices": '$max_devices'
}' \
--name="$file_name-$CODEBUILD_SOURCE_VERSION" \
--test="type=INSTRUMENTATION,testPackageArn=$test_package_upload_arn" \
diff --git a/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousAuth.java b/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousAuth.java
index 82a39be9aa..442ee71436 100644
--- a/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousAuth.java
+++ b/testutils/src/main/java/com/amplifyframework/testutils/sync/SynchronousAuth.java
@@ -49,6 +49,8 @@
import com.amplifyframework.auth.result.AuthUpdateAttributeResult;
import com.amplifyframework.core.Amplify;
import com.amplifyframework.core.plugin.Plugin;
+import com.amplifyframework.logging.AndroidLoggingPlugin;
+import com.amplifyframework.logging.LogLevel;
import com.amplifyframework.testutils.Await;
import com.amplifyframework.testutils.VoidResult;
@@ -107,6 +109,7 @@ public static SynchronousAuth delegatingToCognito(Context context, Plugin> aut
throws AmplifyException, InterruptedException {
try {
Amplify.Auth.addPlugin((AuthPlugin>) authPlugin);
+ Amplify.Logging.addPlugin(new AndroidLoggingPlugin(LogLevel.DEBUG));
Amplify.configure(context);
} catch (Exception exception) {
Log.i("SynchronousAuth", "Amplify already called", exception);