diff --git a/.github/workflows/test-android.yml b/.github/workflows/test-android.yml new file mode 100644 index 0000000..bbc842c --- /dev/null +++ b/.github/workflows/test-android.yml @@ -0,0 +1,29 @@ +name: Run Unit Tests for Android +on: + workflow_dispatch: + pull_request: + branches: [ main ] +jobs: + test-android: + name: Test Android + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Run Unit Tests + run: | + ./gradlew testDebugUnitTest + + - name: Upload test results + uses: actions/upload-artifact@v2 + if: always() + with: + name: test-results + path: library/build/reports/tests/testDebugUnitTest/ + retention-days: 1 \ No newline at end of file diff --git a/README.md b/README.md index 002a309..b73915a 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ # Amazon Location Service Mobile Authentication SDK for Android -These utilities help you authenticate when making [Amazon Location Service](https://aws.amazon.com/location/) API calls from your Android applications. This specifically helps when using [Amazon Cognito](https://docs.aws.amazon.com/location/latest/developerguide/authenticating-using-cognito.html) or [API keys](https://docs.aws.amazon.com/location/latest/developerguide/using-apikeys.html) as the authentication method. +These utilities help you authenticate when making [Amazon Location Service](https://aws.amazon.com/location/) API calls from your Android applications. This specifically helps when using [Amazon Cognito](https://docs.aws.amazon.com/location/latest/developerguide/authenticating-using-cognito.html) as the authentication method. ## Installation -This authentication SDK works with the overall AWS SDK. Both SDKs are published to Maven Central. +This authentication SDK works with the overall AWS Kotlin SDK. Both SDKs are published to Maven Central. Check the [latest version](https://mvnrepository.com/artifact/software.amazon.location/auth) of auth SDK on Maven Central. Add the following lines to the dependencies section of your build.gradle file in Android Studio: ``` -implementation("software.amazon.location:auth:0.0.2") +implementation("software.amazon.location:auth:0.2.4") implementation("aws.sdk.kotlin:location:1.2.21") implementation("org.maplibre.gl:android-sdk:11.0.0-pre5") implementation("com.squareup.okhttp3:okhttp:4.12.0") @@ -34,19 +34,19 @@ import okhttp3.OkHttpClient You can create an AuthHelper and use it with the AWS Kotlin SDK: ``` -// Create an authentication helper instance using an Amazon Location API Key -private fun exampleAPIKeyLogin() { +// Create a credentail provider using Identity Pool Id with AuthHelper +private fun exampleCognitoLogin() { var authHelper = AuthHelper(applicationContext) - var locationCredentialsProvider : LocationCredentialsProvider = authHelper.authenticateWithApiKey("My-Amazon-Location-API-Key") + var locationCredentialsProvider : LocationCredentialsProvider = authHelper.authenticateWithCognitoIdentityPool("My-Cognito-Identity-Pool-Id") var locationClient = locationCredentialsProvider?.getLocationClient() } -``` -``` -// Create an authentication helper using credentials from Cognito -private fun exampleCognitoLogin() { +OR + +// Create a credentail provider using custom credential provider with AuthHelper +private fun exampleCustomCredentialLogin() { var authHelper = AuthHelper(applicationContext) - var locationCredentialsProvider : LocationCredentialsProvider = authHelper.authenticateWithCognitoIdentityPool("My-Cognito-Identity-Pool-Id") + var locationCredentialsProvider : LocationCredentialsProvider = authHelper.authenticateWithCredentialsProvider("MY-AWS-REGION", MY-CUSTOM-CREDENTIAL-PROVIDER) var locationClient = locationCredentialsProvider?.getLocationClient() } ``` @@ -58,7 +58,7 @@ HttpRequestUtil.setOkHttpClient( .addInterceptor( AwsSignerInterceptor( "geo", - "My-aws-region", + "MY-AWS-REGION", locationCredentialsProvider ) ) diff --git a/library/src/main/java/software/amazon/location/auth/ApiKeyCredentialsProvider.kt b/library/src/main/java/software/amazon/location/auth/ApiKeyCredentialsProvider.kt deleted file mode 100644 index f5ccfb7..0000000 --- a/library/src/main/java/software/amazon/location/auth/ApiKeyCredentialsProvider.kt +++ /dev/null @@ -1,59 +0,0 @@ -package software.amazon.location.auth - -import android.content.Context -import software.amazon.location.auth.utils.Constants.API_KEY - -/** - * Provides API key credentials for accessing services and manages their storage. - */ -class ApiKeyCredentialsProvider { - private var securePreferences: EncryptedSharedPreferences? = null - - /** - * Initializes the provider and saves the provided API key. - * @param context The application context. - * @param apiKey The API key to save. - */ - constructor(context: Context, apiKey: String) { - initialize(context) - saveCredentials(apiKey) - } - - /** - * Initializes the provider and retrieves the API key from the cache. - * @param context The application context. - * @throws Exception If no credentials are found in the cache. - */ - constructor(context: Context) { - initialize(context) - val apiKey = getCachedCredentials() - if (apiKey === null) throw Exception("No credentials found") - } - - private fun initialize(context: Context) { - securePreferences = EncryptedSharedPreferences(context, PREFS_NAME) - securePreferences?.initEncryptedSharedPreferences() - } - - private fun saveCredentials(apiKey: String) { - if (securePreferences === null) throw Exception("Not initialized") - securePreferences!!.put(API_KEY, apiKey) - } - - /** - * Retrieves the cached API key credentials. - * @return The API key or null if not found. - * @throws Exception If the AWSKeyValueStore is not initialized. - */ - fun getCachedCredentials(): String? { - if (securePreferences === null) throw Exception("Not initialized") - return securePreferences!!.get(API_KEY) - } - - /** - * Clears the stored credentials. - */ - fun clearCredentials() { - securePreferences?.clear() - } -} \ No newline at end of file diff --git a/library/src/main/java/software/amazon/location/auth/AuthHelper.kt b/library/src/main/java/software/amazon/location/auth/AuthHelper.kt index 39265bc..3aeb83f 100644 --- a/library/src/main/java/software/amazon/location/auth/AuthHelper.kt +++ b/library/src/main/java/software/amazon/location/auth/AuthHelper.kt @@ -1,6 +1,7 @@ package software.amazon.location.auth import android.content.Context +import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import software.amazon.location.auth.utils.AwsRegions @@ -73,14 +74,48 @@ class AuthHelper(private val context: Context) { } /** - * Authenticates using an API key. - * @param apiKey The API key for authentication. - * @return A LocationCredentialsProvider instance. + * Authenticates using a region and a specified CredentialsProvider. + * + * For example, to get credentials from AWS Kotlin SDK: + * 1. Use `CognitoIdentityClient` to call `.getId` to get the identity ID: + * ``` kotlin + * val identityId = cognitoIdentityClient.getId(GetIdRequest { this.identityPoolId = identityPoolId }).identityId + * ``` + * + * 2. Use `CognitoIdentityClient` to call `.getCredentialsForIdentity` with the identity ID to get the credentials: + * ``` kotlin + * val credentials = cognitoIdentityClient.getCredentialsForIdentity(GetCredentialsForIdentityRequest { this.identityId = identityId }).credentials + * ``` + * + * + * To create a `StaticCredentialsProvider` as a `CredentialsProvider` from the obtained credentials: + * 1. Use the credentials obtained in the previous steps: + * ``` kotlin + * val staticCredentialsProvider = StaticCredentialsProvider( + * aws.smithy.kotlin.runtime.auth.awscredentials.Credentials.invoke( + * accessKeyId = credentials.accessKeyId, + * secretAccessKey = credentials.secretKey, + * sessionToken = credentials.sessionToken, + * expiration = credentials.expiration + * ) + * ) + * ``` + * + * @param region The AWS region as a string. + * @param credentialsProvider The `CredentialsProvider` from AWS Kotlin SDK (`aws.smithy.kotlin.runtime.auth.awscredentials`). + * @return A `LocationCredentialsProvider` object. */ - fun authenticateWithApiKey( - apiKey: String, - ): LocationCredentialsProvider = LocationCredentialsProvider( - context, - apiKey, - ) + suspend fun authenticateWithCredentialsProvider( + region: String, + credentialsProvider: CredentialsProvider + ): LocationCredentialsProvider { + return withContext(Dispatchers.IO) { + val locationCredentialsProvider = LocationCredentialsProvider( + context, + AwsRegions.fromName(region), + ) + locationCredentialsProvider.initializeLocationClient(credentialsProvider) + locationCredentialsProvider + } + } } diff --git a/library/src/main/java/software/amazon/location/auth/LocationCredentialsProvider.kt b/library/src/main/java/software/amazon/location/auth/LocationCredentialsProvider.kt index 2bdb221..1ee7006 100644 --- a/library/src/main/java/software/amazon/location/auth/LocationCredentialsProvider.kt +++ b/library/src/main/java/software/amazon/location/auth/LocationCredentialsProvider.kt @@ -11,7 +11,6 @@ import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider import aws.smithy.kotlin.runtime.time.Instant import aws.smithy.kotlin.runtime.time.epochMilliseconds import software.amazon.location.auth.utils.AwsRegions -import software.amazon.location.auth.utils.Constants.API_KEY import software.amazon.location.auth.utils.Constants.IDENTITY_POOL_ID import software.amazon.location.auth.utils.Constants.METHOD import software.amazon.location.auth.utils.Constants.REGION @@ -19,12 +18,13 @@ import software.amazon.location.auth.utils.Constants.REGION const val PREFS_NAME = "software.amazon.location.auth" /** - * Provides credentials for accessing location-based services through Cognito or API key authentication. + * Provides credentials for accessing location-based services through Cognito authentication. */ class LocationCredentialsProvider { + private var credentialsProvider: CredentialsProvider? = null + private var customCredentials: aws.sdk.kotlin.services.cognitoidentity.model.Credentials? = null private var context: Context private var cognitoCredentialsProvider: CognitoCredentialsProvider? = null - private var apiKeyProvider: ApiKeyCredentialsProvider? = null private var securePreferences: EncryptedSharedPreferences private var locationClient: LocationClient? = null private var cognitoIdentityClient: CognitoIdentityClient? = null @@ -37,36 +37,33 @@ class LocationCredentialsProvider { */ constructor(context: Context, identityPoolId: String, region: AwsRegions) { this.context = context - securePreferences = EncryptedSharedPreferences(context, PREFS_NAME) - securePreferences.initEncryptedSharedPreferences() + securePreferences = initPreference(context) securePreferences.put(METHOD, "cognito") securePreferences.put(IDENTITY_POOL_ID, identityPoolId) securePreferences.put(REGION, region.regionName) } /** - * Initializes with an API key. + * Initializes with region. * @param context The application context. - * @param apiKey The API key for authentication. + * @param region The region for Cognito authentication. */ - constructor(context: Context, apiKey: String) { + constructor(context: Context, region: AwsRegions) { this.context = context - securePreferences = EncryptedSharedPreferences(context, PREFS_NAME) - securePreferences.initEncryptedSharedPreferences() - securePreferences.put(METHOD, "apiKey") - securePreferences.put(API_KEY, apiKey) - apiKeyProvider = ApiKeyCredentialsProvider(context, apiKey) + securePreferences = initPreference(context) + securePreferences.put(METHOD, "custom") + securePreferences.put(REGION, region.regionName) } + /** * Initializes with cached credentials. * @param context The application context. - * @throws Exception If API key credentials are not found. + * @throws Exception If credentials are not found. */ constructor(context: Context) { this.context = context - securePreferences = EncryptedSharedPreferences(context, PREFS_NAME) - securePreferences.initEncryptedSharedPreferences() + securePreferences = initPreference(context) val method = securePreferences.get(METHOD) if (method === null) throw Exception("No credentials found") when (method) { @@ -76,19 +73,16 @@ class LocationCredentialsProvider { if (identityPoolId === null || region === null) throw Exception("No credentials found") cognitoCredentialsProvider = CognitoCredentialsProvider(context) } - - "apiKey" -> { - val apiKey = securePreferences.get(API_KEY) - if (apiKey === null) throw Exception("No credentials found") - apiKeyProvider = ApiKeyCredentialsProvider(context, apiKey) - } - else -> { throw Exception("No credentials found") } } } + fun initPreference(context: Context): EncryptedSharedPreferences { + return EncryptedSharedPreferences(context, PREFS_NAME).apply { initEncryptedSharedPreferences() } + } + /** * Checks AWS credentials availability and validity. * @@ -117,6 +111,39 @@ class LocationCredentialsProvider { } } + /** + * Initializes the Location client with custom CredentialsProvider + * + * This method generates a Cognito identity client if not already present, retrieves an identity ID using the + * provided identity pool ID, and initializes the `CognitoCredentialsProvider` with the resolved credentials. + * + * @param credentialsProvider The provider for AWS credentials. + */ + suspend fun initializeLocationClient(credentialsProvider: CredentialsProvider) { + val region = securePreferences.get(REGION) + if (region === null) throw Exception("No credentials found") + + this.credentialsProvider = credentialsProvider + setCustomCredentials(credentialsProvider, region) + } + + /** + * Sets custom credentials using the provided credentials provider and region. + * + * @param credentialsProvider The provider for AWS credentials, which is used to resolve AWS access credentials. + * @param region The AWS region to be used for initializing the location client. + */ + private suspend fun setCustomCredentials(credentialsProvider: CredentialsProvider, region: String) { + val credentials = credentialsProvider.resolve() + customCredentials = aws.sdk.kotlin.services.cognitoidentity.model.Credentials.invoke { + accessKeyId = credentials.accessKeyId + secretKey = credentials.secretAccessKey + sessionToken = credentials.sessionToken + expiration = credentials.expiration + } + locationClient = generateLocationClient(region, credentialsProvider) + } + /** * Retrieves the LocationClient instance with configured AWS credentials. @@ -128,9 +155,8 @@ class LocationCredentialsProvider { * @throws Exception if the AWS region is not found in secure preferences. */ fun getLocationClient(): LocationClient? { - val identityPoolId = securePreferences.get(IDENTITY_POOL_ID) val region = securePreferences.get(REGION) - if (identityPoolId === null || region === null) throw Exception("No credentials found") + if (region === null) throw Exception("No credentials found") if (locationClient == null) { val credentialsProvider = createCredentialsProvider() locationClient = generateLocationClient(region, credentialsProvider) @@ -174,6 +200,7 @@ class LocationCredentialsProvider { accessKeyId = getCredentialsProvider()?.accessKeyId!!, secretAccessKey = getCredentialsProvider()?.secretKey!!, sessionToken = getCredentialsProvider()?.sessionToken, + expiration = getCredentialsProvider()?.expiration ) ) } @@ -225,7 +252,6 @@ class LocationCredentialsProvider { * @param region The AWS region for the CognitoIdentityClient. * @return A new instance of CognitoIdentityClient. */ - fun generateCognitoIdentityClient(region: String): CognitoIdentityClient { return CognitoIdentityClient { this.region = region } } @@ -236,9 +262,12 @@ class LocationCredentialsProvider { * @return True if the credentials are valid (i.e., not expired), false otherwise. */ fun isCredentialsValid(): Boolean { - val credentials = cognitoCredentialsProvider?.getCachedCredentials() + val expirationTimeMillis: Long = if (cognitoCredentialsProvider == null && customCredentials != null) { + customCredentials?.expiration?.epochMilliseconds ?: throw Exception("Failed to get credentials") + } else { + getCredentialsProvider()?.expiration?.epochMilliseconds ?: throw Exception("Failed to get credentials") + } val currentTimeMillis = Instant.now().epochMilliseconds - val expirationTimeMillis = credentials?.expiration?.epochMilliseconds ?: throw Exception("Failed to get credentials") return currentTimeMillis < expirationTimeMillis } @@ -248,34 +277,36 @@ class LocationCredentialsProvider { * @throws Exception If the Cognito provider is not initialized. */ fun getCredentialsProvider(): aws.sdk.kotlin.services.cognitoidentity.model.Credentials? { + val method = securePreferences.get(METHOD) + if (method == "custom" && customCredentials != null) { + return customCredentials + } if (cognitoCredentialsProvider === null) throw Exception("Cognito credentials not initialized") return cognitoCredentialsProvider?.getCachedCredentials() } - /** - * Retrieves the API key credentials provider. - * @return The ApiKeyCredentialsProvider instance. - * @throws Exception If the API key provider is not initialized. - */ - fun getApiKeyProvider(): ApiKeyCredentialsProvider { - if (apiKeyProvider === null) throw Exception("Api key provider not initialized") - return apiKeyProvider!! - } - /** * Refreshes the Cognito credentials. - * @throws Exception If the Cognito provider is not initialized or if called for API key authentication. + * @throws Exception If the Cognito provider is not initialized. */ suspend fun refresh() { - if (cognitoCredentialsProvider === null) throw Exception("Refresh is only supported for Cognito credentials. Make sure to use the cognito constructor.") - locationClient = null - cognitoCredentialsProvider?.clearCredentials() - verifyAndRefreshCredentials() + val region = securePreferences.get(REGION) + if (region === null) throw Exception("No credentials found") + + val method = securePreferences.get(METHOD) + if (method == "custom" && customCredentials != null) { + credentialsProvider?.let { setCustomCredentials(it, region) } + } else { + if (cognitoCredentialsProvider === null) throw Exception("Refresh is only supported for Cognito credentials. Make sure to use the cognito constructor.") + locationClient = null + cognitoCredentialsProvider?.clearCredentials() + verifyAndRefreshCredentials() + } } /** * Clears the Cognito credentials. - * @throws Exception If the Cognito provider is not initialized or if called for API key authentication. + * @throws Exception If the Cognito provider is not initialized. */ fun clear() { if (cognitoCredentialsProvider === null) throw Exception("Clear is only supported for Cognito credentials. Make sure to use the cognito constructor.") diff --git a/library/src/main/java/software/amazon/location/auth/utils/Constants.kt b/library/src/main/java/software/amazon/location/auth/utils/Constants.kt index d22fb39..1ce1eaa 100644 --- a/library/src/main/java/software/amazon/location/auth/utils/Constants.kt +++ b/library/src/main/java/software/amazon/location/auth/utils/Constants.kt @@ -17,6 +17,5 @@ object Constants { const val REGION= "region" const val METHOD= "method" const val IDENTITY_POOL_ID= "identityPoolId" - const val API_KEY = "apiKey" const val DEFAULT_ENCODING = "UTF-8" } \ No newline at end of file diff --git a/library/src/test/java/software/amazon/location/auth/ApiKeyCredentialsProviderTest.kt b/library/src/test/java/software/amazon/location/auth/ApiKeyCredentialsProviderTest.kt deleted file mode 100644 index 676ed0b..0000000 --- a/library/src/test/java/software/amazon/location/auth/ApiKeyCredentialsProviderTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package software.amazon.location.auth - -import android.content.Context -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.mockkConstructor -import io.mockk.runs -import io.mockk.verify -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import org.junit.Before -import org.junit.Test - -private const val TEST_API_KEY = "dummyApiKey" - -class ApiKeyCredentialsProviderTest { - - private lateinit var context: Context - @Before - fun setUp() { - context = mockk(relaxed = true) - - mockkConstructor(EncryptedSharedPreferences::class) - - every { anyConstructed().initEncryptedSharedPreferences() } just runs - every { anyConstructed().put(any(), any()) } just runs - every { anyConstructed().get("region") } returns "us-east-1" - every { anyConstructed().clear() } just runs - } - - @Test - fun `constructor with apiKey saves credentials`() { - every { anyConstructed().put("apiKey", TEST_API_KEY) } just runs - - ApiKeyCredentialsProvider(context, TEST_API_KEY) - - verify { anyConstructed().put("apiKey", TEST_API_KEY) } - } - - @Test - fun `constructor without apiKey throws when no credentials found`() { - every { anyConstructed().get("apiKey") } returns null - - assertFailsWith { - ApiKeyCredentialsProvider(context) - } - } - - @Test - fun `getCachedCredentials returns apiKey when found`() { - val apiKey = "testApiKey" - every { anyConstructed().get("apiKey") } returns apiKey - - val provider = ApiKeyCredentialsProvider(context, apiKey) - val cachedApiKey = provider.getCachedCredentials() - - assertEquals(apiKey, cachedApiKey) - } - - @Test - fun `getCachedCredentials throws when not initialized`() { - val provider = ApiKeyCredentialsProvider(context, "testApiKey") - - every { anyConstructed().get("apiKey") } throws Exception("Not initialized") - - assertFailsWith { - provider.getCachedCredentials() - } - } - - @Test - fun `clearCredentials clears the stored credentials`() { - val provider = ApiKeyCredentialsProvider(context, "testApiKey") - - provider.clearCredentials() - - verify { anyConstructed().clear() } - } -} \ No newline at end of file diff --git a/library/src/test/java/software/amazon/location/auth/AuthHelperTest.kt b/library/src/test/java/software/amazon/location/auth/AuthHelperTest.kt index 203a9d8..72164e1 100644 --- a/library/src/test/java/software/amazon/location/auth/AuthHelperTest.kt +++ b/library/src/test/java/software/amazon/location/auth/AuthHelperTest.kt @@ -1,6 +1,9 @@ package software.amazon.location.auth import android.content.Context +import aws.sdk.kotlin.services.cognitoidentity.CognitoIdentityClient +import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import io.mockk.coEvery import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -9,39 +12,49 @@ import io.mockk.runs import org.junit.Before import org.junit.Test import kotlin.test.assertNotNull -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import software.amazon.location.auth.utils.AwsRegions import software.amazon.location.auth.utils.Constants +import software.amazon.location.auth.utils.Constants.IDENTITY_POOL_ID import software.amazon.location.auth.utils.Constants.TEST_IDENTITY_POOL_ID -private const val TEST_API_KEY = "dummyApiKey" - class AuthHelperTest { private lateinit var context: Context private lateinit var authHelper: AuthHelper - private val coroutineScope = CoroutineScope(Dispatchers.Default) + private lateinit var credentialsProvider: CredentialsProvider + private lateinit var encryptedSharedPreferences: EncryptedSharedPreferences + private lateinit var cognitoIdentityClient: CognitoIdentityClient @Before fun setUp() { context = mockk(relaxed = true) authHelper = AuthHelper(context) + credentialsProvider = mockk(relaxed = true) + encryptedSharedPreferences = mockk(relaxed = true) + cognitoIdentityClient = mockk(relaxed = true) mockkConstructor(EncryptedSharedPreferences::class) - mockkConstructor(EncryptedSharedPreferences::class) + mockkConstructor(LocationCredentialsProvider::class) - every { anyConstructed().initEncryptedSharedPreferences() } just runs every { anyConstructed().put(any(), any()) } just runs every { anyConstructed().clear() } just runs every { anyConstructed().remove(any()) } just runs every { anyConstructed().get("region") } returns "us-east-1" - every { anyConstructed().get("identityPoolId") } returns TEST_IDENTITY_POOL_ID + every { anyConstructed().get(Constants.ACCESS_KEY_ID) } returns "test" + every { anyConstructed().get(Constants.SECRET_KEY) } returns "test" + every { anyConstructed().get(Constants.SESSION_TOKEN) } returns "test" + every { anyConstructed().get(Constants.EXPIRATION) } returns "11111" + every { anyConstructed().get(IDENTITY_POOL_ID) } returns TEST_IDENTITY_POOL_ID + every { anyConstructed().generateCognitoIdentityClient("us-east-1") } returns cognitoIdentityClient + coEvery { anyConstructed().initializeLocationClient(any()) } just runs + coEvery { anyConstructed().isCredentialsValid() } returns true + every { anyConstructed().initPreference(context) } returns encryptedSharedPreferences + every { anyConstructed().initEncryptedSharedPreferences() } just runs } @Test fun `authenticateWithCognitoIdentityPool with identityPoolId creates LocationCredentialsProvider`() { - coroutineScope.launch { + runBlocking { val provider = authHelper.authenticateWithCognitoIdentityPool(TEST_IDENTITY_POOL_ID) assertNotNull(provider) } @@ -49,11 +62,7 @@ class AuthHelperTest { @Test fun `authenticateWithCognitoIdentityPool with identityPoolId and string region creates LocationCredentialsProvider`() { - every { anyConstructed().get(Constants.ACCESS_KEY_ID) } returns "test" - every { anyConstructed().get(Constants.SECRET_KEY) } returns "test" - every { anyConstructed().get(Constants.SESSION_TOKEN) } returns "test" - every { anyConstructed().get(Constants.EXPIRATION) } returns "11111" - coroutineScope.launch { + runBlocking { val provider = authHelper.authenticateWithCognitoIdentityPool(TEST_IDENTITY_POOL_ID, "us-east-1") assertNotNull(provider) @@ -62,7 +71,7 @@ class AuthHelperTest { @Test fun `authenticateWithCognitoIdentityPool with identityPoolId and Regions enum creates LocationCredentialsProvider`() { - coroutineScope.launch { + runBlocking { val provider = authHelper.authenticateWithCognitoIdentityPool( TEST_IDENTITY_POOL_ID, @@ -73,8 +82,14 @@ class AuthHelperTest { } @Test - fun `authenticateWithApiKey creates LocationCredentialsProvider`() { - val provider = authHelper.authenticateWithApiKey(TEST_API_KEY) - assertNotNull(provider) + fun `authenticateWithCredentialsProvider with identityPoolId`() { + runBlocking { + val provider = + authHelper.authenticateWithCredentialsProvider( + "us-east-1", + credentialsProvider + ) + assertNotNull(provider) + } } } \ No newline at end of file diff --git a/library/src/test/java/software/amazon/location/auth/CustomCredentialsProviderTest.kt b/library/src/test/java/software/amazon/location/auth/CustomCredentialsProviderTest.kt new file mode 100644 index 0000000..b68c275 --- /dev/null +++ b/library/src/test/java/software/amazon/location/auth/CustomCredentialsProviderTest.kt @@ -0,0 +1,87 @@ +package software.amazon.location.auth + +import android.content.Context +import aws.sdk.kotlin.services.cognitoidentity.CognitoIdentityClient +import aws.sdk.kotlin.services.cognitoidentity.model.Credentials +import aws.sdk.kotlin.services.location.LocationClient +import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.time.Instant +import aws.smithy.kotlin.runtime.time.epochMilliseconds +import aws.smithy.kotlin.runtime.time.fromEpochMilliseconds +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkConstructor +import io.mockk.runs +import junit.framework.TestCase.assertNotNull +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import software.amazon.location.auth.utils.AwsRegions +import software.amazon.location.auth.utils.Constants.ACCESS_KEY_ID +import software.amazon.location.auth.utils.Constants.EXPIRATION +import software.amazon.location.auth.utils.Constants.IDENTITY_POOL_ID +import software.amazon.location.auth.utils.Constants.METHOD +import software.amazon.location.auth.utils.Constants.REGION +import software.amazon.location.auth.utils.Constants.SECRET_KEY +import software.amazon.location.auth.utils.Constants.SESSION_TOKEN +import software.amazon.location.auth.utils.Constants.TEST_IDENTITY_POOL_ID + +class CustomCredentialsProviderTest { + private lateinit var context: Context + private lateinit var locationClient: LocationClient + private lateinit var cognitoIdentityClient: CognitoIdentityClient + private lateinit var cognitoCredentialsProvider: CognitoCredentialsProvider + private lateinit var credentialsProvider: CredentialsProvider + + @Before + fun setUp() { + context = mockk(relaxed = true) + locationClient = mockk(relaxed = true) + cognitoIdentityClient = mockk(relaxed = true) + cognitoCredentialsProvider = mockk(relaxed = true) + credentialsProvider = mockk(relaxed = true) + mockkConstructor(EncryptedSharedPreferences::class) + mockkConstructor(CognitoCredentialsProvider::class) + mockkConstructor(LocationCredentialsProvider::class) + every { anyConstructed().initEncryptedSharedPreferences() } just runs + + every { anyConstructed().generateCognitoIdentityClient("us-east-1") } returns cognitoIdentityClient + every { + anyConstructed().generateLocationClient( + "us-east-1", + any(), + ) + } returns locationClient + every { anyConstructed().put(any(), any()) } just runs + every { anyConstructed().get(REGION) } returns "us-east-1" + every { anyConstructed().clear() } just runs + every { anyConstructed().remove(any()) } just runs + } + + @Test + fun `getCredentialsProvider returns cognito provider successfully with custom credential`() { + val expirationTime = + Instant.fromEpochMilliseconds(Instant.now().epochMilliseconds + 10000) // 10 seconds in the future + val mockCredentials = + Credentials.invoke { + expiration = expirationTime + secretKey = "test" + accessKeyId = "test" + sessionToken = "test" + } + every { anyConstructed().getCachedCredentials() } returns mockCredentials + every { anyConstructed().get(METHOD) } returns "" + every { anyConstructed().get(IDENTITY_POOL_ID) } returns TEST_IDENTITY_POOL_ID + every { anyConstructed().get(ACCESS_KEY_ID) } returns "test" + every { anyConstructed().get(SECRET_KEY) } returns "test" + every { anyConstructed().get(SESSION_TOKEN) } returns "test" + every { anyConstructed().get(EXPIRATION) } returns "11111" + val provider = + LocationCredentialsProvider(context, TEST_IDENTITY_POOL_ID, AwsRegions.US_EAST_1) + runBlocking { + provider.verifyAndRefreshCredentials() + assertNotNull(provider.getCredentialsProvider()) + } + } +} diff --git a/library/src/test/java/software/amazon/location/auth/LocationCredentialsProviderTest.kt b/library/src/test/java/software/amazon/location/auth/LocationCredentialsProviderTest.kt index b289b74..4527bee 100644 --- a/library/src/test/java/software/amazon/location/auth/LocationCredentialsProviderTest.kt +++ b/library/src/test/java/software/amazon/location/auth/LocationCredentialsProviderTest.kt @@ -23,15 +23,11 @@ import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertNotNull import kotlin.test.assertFailsWith import kotlin.test.assertTrue -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test import software.amazon.location.auth.utils.AwsRegions import software.amazon.location.auth.utils.Constants.ACCESS_KEY_ID -import software.amazon.location.auth.utils.Constants.API_KEY import software.amazon.location.auth.utils.Constants.EXPIRATION import software.amazon.location.auth.utils.Constants.IDENTITY_POOL_ID import software.amazon.location.auth.utils.Constants.METHOD @@ -40,16 +36,12 @@ import software.amazon.location.auth.utils.Constants.SECRET_KEY import software.amazon.location.auth.utils.Constants.SESSION_TOKEN import software.amazon.location.auth.utils.Constants.TEST_IDENTITY_POOL_ID -private const val TEST_API_KEY = "dummyApiKey" - class LocationCredentialsProviderTest { - private lateinit var context: Context private lateinit var locationClient: LocationClient private lateinit var cognitoIdentityClient: CognitoIdentityClient private lateinit var cognitoCredentialsProvider: CognitoCredentialsProvider private lateinit var credentialsProvider: CredentialsProvider - private val coroutineScope = CoroutineScope(Dispatchers.Default) @Before fun setUp() { @@ -64,27 +56,18 @@ class LocationCredentialsProviderTest { every { anyConstructed().initEncryptedSharedPreferences() } just runs every { anyConstructed().generateCognitoIdentityClient("us-east-1") } returns cognitoIdentityClient - every { anyConstructed().generateLocationClient("us-east-1", any()) } returns locationClient + every { + anyConstructed().generateLocationClient( + "us-east-1", + any(), + ) + } returns locationClient every { anyConstructed().put(any(), any()) } just runs every { anyConstructed().get(REGION) } returns "us-east-1" every { anyConstructed().clear() } just runs every { anyConstructed().remove(any()) } just runs } - @Test - fun `constructor with Cognito initializes correctly`() { - every { anyConstructed().get(METHOD) } returns "api" - val provider = - LocationCredentialsProvider(context, TEST_IDENTITY_POOL_ID, AwsRegions.US_EAST_1) - assertNotNull(provider) - } - - @Test - fun `constructor with API key initializes correctly`() { - val provider = LocationCredentialsProvider(context, TEST_API_KEY) - assertNotNull(provider) - } - @Test fun `constructor with cached credentials for Cognito initializes correctly`() { every { anyConstructed().get(METHOD) } returns "cognito" @@ -97,16 +80,19 @@ class LocationCredentialsProviderTest { assertNotNull(provider) } - @Test - fun `constructor with cached credentials for API key initializes correctly`() { - every { anyConstructed().get(METHOD) } returns "apiKey" - every { anyConstructed().get(API_KEY) } returns TEST_API_KEY - val provider = LocationCredentialsProvider(context) - assertNotNull(provider) - } - @Test fun `getCredentialsProvider returns cognito provider successfully`() { + val expirationTime = + Instant.fromEpochMilliseconds(Instant.now().epochMilliseconds + 10000) // 10 seconds in the future + val mockCredentials = + Credentials.invoke { + expiration = expirationTime + secretKey = "test" + accessKeyId = "test" + sessionToken = "test" + } + every { anyConstructed().getCachedCredentials() } returns mockCredentials + every { anyConstructed().get(METHOD) } returns "" every { anyConstructed().get(IDENTITY_POOL_ID) } returns TEST_IDENTITY_POOL_ID every { anyConstructed().get(ACCESS_KEY_ID) } returns "test" every { anyConstructed().get(SECRET_KEY) } returns "test" @@ -121,22 +107,44 @@ class LocationCredentialsProviderTest { } @Test - fun `getApiKeyProvider returns api key provider successfully`() { - val provider = LocationCredentialsProvider(context, TEST_API_KEY) - assertNotNull(provider.getApiKeyProvider()) + fun `initializeLocationClient`() { + val expirationTime = + Instant.fromEpochMilliseconds(Instant.now().epochMilliseconds + 10000) // 10 seconds in the future + val mockCredentials = + Credentials.invoke { + expiration = expirationTime + secretKey = "test" + accessKeyId = "test" + sessionToken = "test" + } + every { anyConstructed().getCachedCredentials() } returns mockCredentials + every { anyConstructed().get(METHOD) } returns "" + every { anyConstructed().get(IDENTITY_POOL_ID) } returns TEST_IDENTITY_POOL_ID + every { anyConstructed().get(ACCESS_KEY_ID) } returns "test" + every { anyConstructed().get(SECRET_KEY) } returns "test" + every { anyConstructed().get(SESSION_TOKEN) } returns "test" + every { anyConstructed().get(EXPIRATION) } returns "11111" + val provider = + LocationCredentialsProvider(context, TEST_IDENTITY_POOL_ID, AwsRegions.US_EAST_1) + runBlocking { + provider.initializeLocationClient(credentialsProvider) + assertNotNull(provider.getLocationClient()) + } } @Test fun `isCredentialsValid returns true when credentials are valid`() { val expirationTime = Instant.fromEpochMilliseconds(Instant.now().epochMilliseconds + 10000) // 10 seconds in the future - val mockCredentials = Credentials.invoke { - expiration = expirationTime - secretKey = "test" - accessKeyId = "test" - sessionToken = "test" - } + val mockCredentials = + Credentials.invoke { + expiration = expirationTime + secretKey = "test" + accessKeyId = "test" + sessionToken = "test" + } every { anyConstructed().getCachedCredentials() } returns mockCredentials + every { anyConstructed().get(METHOD) } returns "" every { anyConstructed().get(METHOD) } returns "cognito" every { anyConstructed().get(ACCESS_KEY_ID) } returns "test" every { anyConstructed().get(SECRET_KEY) } returns "test" @@ -157,9 +165,10 @@ class LocationCredentialsProviderTest { fun `isCredentialsValid returns false when credentials are expired`() { val expirationTime = Instant.fromEpochMilliseconds(Instant.now().epochMilliseconds - 10000) // 10 seconds in the past - val mockCredentials = mockk { - every { expiration } returns expirationTime - } + val mockCredentials = + mockk { + every { expiration } returns expirationTime + } every { anyConstructed().getCachedCredentials() } returns mockCredentials every { anyConstructed().get(METHOD) } returns "cognito" every { anyConstructed().get(ACCESS_KEY_ID) } returns "test" @@ -213,23 +222,26 @@ class LocationCredentialsProviderTest { every { anyConstructed().get(EXPIRATION) } returns "11111" every { anyConstructed().get(IDENTITY_POOL_ID) } returns TEST_IDENTITY_POOL_ID val identityId = "test-identity-id" - val credentials = Credentials { - accessKeyId = "test-access-key" - secretKey = "test-secret-key" - sessionToken = "test-session-token" - } + val credentials = + Credentials { + accessKeyId = "test-access-key" + secretKey = "test-secret-key" + sessionToken = "test-session-token" + } every { anyConstructed().get(METHOD) } returns "cognito" every { anyConstructed().get(IDENTITY_POOL_ID) } returns TEST_IDENTITY_POOL_ID val provider = LocationCredentialsProvider(context, TEST_IDENTITY_POOL_ID, AwsRegions.US_EAST_1) - coEvery { cognitoIdentityClient.getId(any()) } returns GetIdResponse { - this.identityId = identityId - } + coEvery { cognitoIdentityClient.getId(any()) } returns + GetIdResponse { + this.identityId = identityId + } - coEvery { cognitoIdentityClient.getCredentialsForIdentity(any()) } returns GetCredentialsForIdentityResponse { - this.credentials = credentials - } + coEvery { cognitoIdentityClient.getCredentialsForIdentity(any()) } returns + GetCredentialsForIdentityResponse { + this.credentials = credentials + } runBlocking { provider.verifyAndRefreshCredentials() val locationClient = provider.getLocationClient() @@ -244,13 +256,6 @@ class LocationCredentialsProviderTest { assertFailsWith { LocationCredentialsProvider(context) } } - @Test - fun `constructor with cached API key credentials throws exception on missing data`() { - every { anyConstructed().get(METHOD) } returns "apiKey" - every { anyConstructed().get(API_KEY) } returns null // Simulate missing data - assertFailsWith { LocationCredentialsProvider(context) } - } - @Test fun `verify SecurePreferences interactions for cognito initialization`() { LocationCredentialsProvider(context, TEST_IDENTITY_POOL_ID, AwsRegions.US_EAST_1) @@ -258,42 +263,14 @@ class LocationCredentialsProviderTest { verify(exactly = 1) { anyConstructed().put( IDENTITY_POOL_ID, - TEST_IDENTITY_POOL_ID + TEST_IDENTITY_POOL_ID, ) } verify(exactly = 1) { anyConstructed().put( REGION, - AwsRegions.US_EAST_1.regionName + AwsRegions.US_EAST_1.regionName, ) } } - - @Test - fun `getCredentialsProvider throws if Cognito provider not initialized`() { - val provider = LocationCredentialsProvider(context, "apiKey") - assertFailsWith { provider.getCredentialsProvider() } - } - - @Test - fun `getApiKeyProvider throws if API key provider not initialized`() { - val provider = - LocationCredentialsProvider(context, TEST_IDENTITY_POOL_ID, AwsRegions.US_EAST_1) - assertFailsWith { provider.getApiKeyProvider() } - } - - @Test - fun `refresh throws if Cognito provider not initialized`() { - val provider = LocationCredentialsProvider(context, "apiKey") - coroutineScope.launch { - provider.verifyAndRefreshCredentials() - assertFailsWith { provider.refresh() } - } - } - - @Test - fun `clear throws if Cognito provider not initialized`() { - val provider = LocationCredentialsProvider(context, "apiKey") - assertFailsWith { provider.clear() } - } } diff --git a/library/src/test/java/software/amazon/location/auth/utils/Constants.kt b/library/src/test/java/software/amazon/location/auth/utils/Constants.kt index 9ca42fe..972f202 100644 --- a/library/src/test/java/software/amazon/location/auth/utils/Constants.kt +++ b/library/src/test/java/software/amazon/location/auth/utils/Constants.kt @@ -7,13 +7,12 @@ object Constants { const val EXPIRATION = "expiration" const val METHOD = "method" const val IDENTITY_POOL_ID = "identityPoolId" - const val API_KEY = "apiKey" const val REGION = "region" const val HEADER_HOST = "Host" const val HEADER_X_AMZ_SECURITY_TOKEN = "x-amz-security-token" const val HEADER_AUTHORIZATION = "Authorization" const val TEST_REGION = "us-west-2" - const val TEST_URL = "https://service.amazonaws.com" + const val TEST_URL = "https://service.amazonaws.com/?test=query" const val TEST_URL1 = "https://example.com" const val TEST_IDENTITY_POOL_ID = "us-east-1:dummyIdentityPoolId" } \ No newline at end of file