diff --git a/authenticator/api/authenticator.api b/authenticator/api/authenticator.api index d607f24d..b597ce78 100644 --- a/authenticator/api/authenticator.api +++ b/authenticator/api/authenticator.api @@ -961,3 +961,7 @@ public final class com/amplifyframework/ui/authenticator/util/MissingConfigurati public fun ()V } +public final class com/amplifyframework/ui/authenticator/util/UnsupportedNextStepException : com/amplifyframework/auth/AuthException { + public static final field $stable I +} + diff --git a/authenticator/build.gradle.kts b/authenticator/build.gradle.kts index 44fa12e7..32b6c658 100644 --- a/authenticator/build.gradle.kts +++ b/authenticator/build.gradle.kts @@ -6,6 +6,7 @@ android { namespace = "com.amplifyframework.ui.authenticator" defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles += file("consumer-rules.pro") } compileOptions { diff --git a/authenticator/consumer-rules.pro b/authenticator/consumer-rules.pro new file mode 100644 index 00000000..04073aab --- /dev/null +++ b/authenticator/consumer-rules.pro @@ -0,0 +1,2 @@ +# Keep AuthExceptions names since these can be mapped to error strings reflectively +-keepnames class * extends com.amplifyframework.auth.AuthException \ No newline at end of file diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt index 5930db31..b16a7d05 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt @@ -79,6 +79,7 @@ import com.amplifyframework.ui.authenticator.util.PasswordResetMessage import com.amplifyframework.ui.authenticator.util.RealAuthProvider import com.amplifyframework.ui.authenticator.util.UnableToResetPasswordMessage import com.amplifyframework.ui.authenticator.util.UnknownErrorMessage +import com.amplifyframework.ui.authenticator.util.UnsupportedNextStepException import com.amplifyframework.ui.authenticator.util.isConnectivityIssue import com.amplifyframework.ui.authenticator.util.toFieldError import kotlinx.coroutines.channels.BufferOverflow @@ -92,7 +93,6 @@ import org.jetbrains.annotations.VisibleForTesting internal class AuthenticatorViewModel(application: Application, private val authProvider: AuthProvider) : AndroidViewModel(application) { - // Constructor for compose viewModels provider constructor(application: Application) : this(application, RealAuthProvider()) @@ -218,6 +218,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth } private suspend fun handleSignUpFailure(error: AuthException) = handleAuthException(error) + private suspend fun handleSignUpConfirmFailure(error: AuthException) = handleAuthException(error) private suspend fun handleSignUpSuccess(username: String, password: String, result: AuthSignUpResult) { @@ -439,10 +440,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth ) else -> { // Generic error for any other next steps that may be added in the future - val exception = AuthException( - "Unsupported next step $nextStep.", - "Authenticator does not support this Authentication flow, disable it to use Authenticator." - ) + val exception = UnsupportedNextStepException(nextStep) logger.error("Unsupported next step $nextStep", exception) sendMessage(UnknownErrorMessage(exception)) } diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/strings/StringResolver.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/strings/StringResolver.kt index 2626cc9b..2c021fb0 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/strings/StringResolver.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/strings/StringResolver.kt @@ -15,9 +15,10 @@ package com.amplifyframework.ui.authenticator.strings +import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import com.amplifyframework.auth.AuthException @@ -27,8 +28,13 @@ import com.amplifyframework.ui.authenticator.forms.FieldError import com.amplifyframework.ui.authenticator.forms.FieldKey import com.amplifyframework.ui.authenticator.forms.PasswordError import com.amplifyframework.ui.authenticator.locals.LocalStringResolver +import com.amplifyframework.ui.authenticator.util.toResourceName +import kotlin.reflect.KClass internal open class StringResolver { + // Avoid recomputing the same error message for each type of exception + private val cachedErrorMessages = mutableMapOf, String>() + @Composable @ReadOnlyComposable open fun label(config: FieldConfig): String { @@ -41,54 +47,49 @@ internal open class StringResolver { @Composable @ReadOnlyComposable - private fun title(config: FieldConfig): String { - return config.label ?: when (config.key) { - FieldKey.ConfirmPassword -> stringResource(R.string.amplify_ui_authenticator_field_label_password_confirm) - FieldKey.ConfirmationCode -> stringResource(R.string.amplify_ui_authenticator_field_label_confirmation_code) - FieldKey.Password -> stringResource(R.string.amplify_ui_authenticator_field_label_password) - FieldKey.PhoneNumber -> stringResource(R.string.amplify_ui_authenticator_field_label_phone_number) - FieldKey.Email -> stringResource(R.string.amplify_ui_authenticator_field_label_email) - FieldKey.Username -> stringResource(R.string.amplify_ui_authenticator_field_label_username) - FieldKey.Birthdate -> stringResource(R.string.amplify_ui_authenticator_field_label_birthdate) - FieldKey.FamilyName -> stringResource(R.string.amplify_ui_authenticator_field_label_family_name) - FieldKey.GivenName -> stringResource(R.string.amplify_ui_authenticator_field_label_given_name) - FieldKey.MiddleName -> stringResource(R.string.amplify_ui_authenticator_field_label_middle_name) - FieldKey.Name -> stringResource(R.string.amplify_ui_authenticator_field_label_name) - FieldKey.Website -> stringResource(R.string.amplify_ui_authenticator_field_label_website) - FieldKey.PhoneNumber -> stringResource(R.string.amplify_ui_authenticator_field_label_phone_number) - FieldKey.Nickname -> stringResource(R.string.amplify_ui_authenticator_field_label_nickname) - FieldKey.PreferredUsername -> - stringResource(R.string.amplify_ui_authenticator_field_label_preferred_username) - FieldKey.Profile -> stringResource(R.string.amplify_ui_authenticator_field_label_profile) - FieldKey.VerificationAttribute -> - stringResource(R.string.amplify_ui_authenticator_field_label_verification_attribute) - else -> "" - } + private fun title(config: FieldConfig): String = config.label ?: when (config.key) { + FieldKey.ConfirmPassword -> stringResource(R.string.amplify_ui_authenticator_field_label_password_confirm) + FieldKey.ConfirmationCode -> stringResource(R.string.amplify_ui_authenticator_field_label_confirmation_code) + FieldKey.Password -> stringResource(R.string.amplify_ui_authenticator_field_label_password) + FieldKey.PhoneNumber -> stringResource(R.string.amplify_ui_authenticator_field_label_phone_number) + FieldKey.Email -> stringResource(R.string.amplify_ui_authenticator_field_label_email) + FieldKey.Username -> stringResource(R.string.amplify_ui_authenticator_field_label_username) + FieldKey.Birthdate -> stringResource(R.string.amplify_ui_authenticator_field_label_birthdate) + FieldKey.FamilyName -> stringResource(R.string.amplify_ui_authenticator_field_label_family_name) + FieldKey.GivenName -> stringResource(R.string.amplify_ui_authenticator_field_label_given_name) + FieldKey.MiddleName -> stringResource(R.string.amplify_ui_authenticator_field_label_middle_name) + FieldKey.Name -> stringResource(R.string.amplify_ui_authenticator_field_label_name) + FieldKey.Website -> stringResource(R.string.amplify_ui_authenticator_field_label_website) + FieldKey.PhoneNumber -> stringResource(R.string.amplify_ui_authenticator_field_label_phone_number) + FieldKey.Nickname -> stringResource(R.string.amplify_ui_authenticator_field_label_nickname) + FieldKey.PreferredUsername -> + stringResource(R.string.amplify_ui_authenticator_field_label_preferred_username) + FieldKey.Profile -> stringResource(R.string.amplify_ui_authenticator_field_label_profile) + FieldKey.VerificationAttribute -> + stringResource(R.string.amplify_ui_authenticator_field_label_verification_attribute) + else -> "" } @Composable @ReadOnlyComposable - open fun hint(config: FieldConfig): String? { - return config.hint ?: when { - config.key == FieldKey.ConfirmPassword -> - stringResource(R.string.amplify_ui_authenticator_field_hint_password_confirm) - config is FieldConfig.Date -> "yyyy-mm-dd" - else -> { - val label = label(config) - stringResource(R.string.amplify_ui_authenticator_field_hint, label) - } + open fun hint(config: FieldConfig): String? = config.hint ?: when { + config.key == FieldKey.ConfirmPassword -> + stringResource(R.string.amplify_ui_authenticator_field_hint_password_confirm) + config is FieldConfig.Date -> "yyyy-mm-dd" + else -> { + val label = label(config) + stringResource(R.string.amplify_ui_authenticator_field_hint, label) } } - @OptIn(ExperimentalComposeUiApi::class) @Composable @ReadOnlyComposable - open fun error(config: FieldConfig, error: FieldError): String { - return when (error) { - is FieldError.InvalidPassword -> { - var errorText = stringResource(R.string.amplify_ui_authenticator_field_password_requirements) - error.errors.forEach { - errorText += "\n" + when (it) { + open fun error(config: FieldConfig, error: FieldError): String = when (error) { + is FieldError.InvalidPassword -> { + var errorText = stringResource(R.string.amplify_ui_authenticator_field_password_requirements) + error.errors.forEach { + errorText += "\n" + + when (it) { is PasswordError.InvalidPasswordLength -> pluralStringResource( id = R.plurals.amplify_ui_authenticator_field_password_too_short, @@ -105,54 +106,57 @@ internal open class StringResolver { stringResource(R.string.amplify_ui_authenticator_field_password_missing_lower) else -> "" } - } - errorText - } - FieldError.PasswordsDoNotMatch -> - stringResource(R.string.amplify_ui_authenticator_field_warn_unmatched_password) - FieldError.MissingRequired -> { - val label = title(config) - stringResource(R.string.amplify_ui_authenticator_field_warn_empty, label) - } - FieldError.InvalidFormat -> { - val label = title(config) - stringResource(R.string.amplify_ui_authenticator_field_warn_invalid_format, label) - } - FieldError.FieldValueExists -> { - val label = title(config) - stringResource(R.string.amplify_ui_authenticator_field_warn_existing, label) - } - FieldError.ConfirmationCodeIncorrect -> { - stringResource(R.string.amplify_ui_authenticator_field_warn_incorrect_code) } - is FieldError.Custom -> error.message - FieldError.NotFound -> { - val label = title(config) - stringResource(R.string.amplify_ui_authenticator_field_warn_not_found, label) - } - else -> "" + errorText + } + FieldError.PasswordsDoNotMatch -> + stringResource(R.string.amplify_ui_authenticator_field_warn_unmatched_password) + FieldError.MissingRequired -> { + val label = title(config) + stringResource(R.string.amplify_ui_authenticator_field_warn_empty, label) + } + FieldError.InvalidFormat -> { + val label = title(config) + stringResource(R.string.amplify_ui_authenticator_field_warn_invalid_format, label) + } + FieldError.FieldValueExists -> { + val label = title(config) + stringResource(R.string.amplify_ui_authenticator_field_warn_existing, label) + } + FieldError.ConfirmationCodeIncorrect -> { + stringResource(R.string.amplify_ui_authenticator_field_warn_incorrect_code) + } + is FieldError.Custom -> error.message + FieldError.NotFound -> { + val label = title(config) + stringResource(R.string.amplify_ui_authenticator_field_warn_not_found, label) } + else -> "" } - @Suppress("UNUSED_EXPRESSION") + @SuppressLint("DiscouragedApi") @Composable @ReadOnlyComposable open fun error(error: AuthException): String { - return when (error) { - else -> stringResource(R.string.amplify_ui_authenticator_error_unknown) + val context = LocalContext.current + return cachedErrorMessages.getOrPut(error::class) { + // Check if the customer application has defined a specific string for this Exception type. If not, return + // the generic error message. + val resourceName = error.toResourceName() + val resourceId = context.resources.getIdentifier(resourceName, "string", context.packageName) + val message = if (resourceId != 0) stringResource(resourceId) else null + message ?: stringResource(R.string.amplify_ui_authenticator_error_unknown) } } companion object { @Composable @ReadOnlyComposable - fun label(config: FieldConfig) = - LocalStringResolver.current.label(config = config) + fun label(config: FieldConfig) = LocalStringResolver.current.label(config = config) @Composable @ReadOnlyComposable - fun hint(config: FieldConfig) = - LocalStringResolver.current.hint(config = config) + fun hint(config: FieldConfig) = LocalStringResolver.current.hint(config = config) @Composable @ReadOnlyComposable @@ -161,7 +165,6 @@ internal open class StringResolver { @Composable @ReadOnlyComposable - fun error(error: AuthException) = - LocalStringResolver.current.error(error = error) + fun error(error: AuthException) = LocalStringResolver.current.error(error = error) } } diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/AuthenticatorMessage.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/AuthenticatorMessage.kt index f19325cf..48f18e10 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/AuthenticatorMessage.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/AuthenticatorMessage.kt @@ -15,11 +15,13 @@ package com.amplifyframework.ui.authenticator.util +import android.annotation.SuppressLint import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.amplifyframework.auth.AuthException import com.amplifyframework.ui.authenticator.R +import kotlin.reflect.KClass /** * Messages that may be displayed in the Authenticator UI. @@ -52,9 +54,7 @@ interface AuthenticatorMessage { } } -internal abstract class AuthenticatorMessageImpl( - private val resource: Int -) : AuthenticatorMessage { +internal abstract class AuthenticatorMessageImpl(protected val resource: Int) : AuthenticatorMessage { override val message: String @Composable @@ -83,48 +83,68 @@ internal object CodeSentMessage : /** * The user cannot reset their password because their account is in an invalid state. */ -internal class UnableToResetPasswordMessage( - override val cause: AuthException -) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_cannot_reset_password), AuthenticatorMessage.Error +internal class UnableToResetPasswordMessage(override val cause: AuthException) : + AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_cannot_reset_password), + AuthenticatorMessage.Error + +// Avoid recomputing the same error message multiple times +private typealias ErrorCache = MutableMap, String> +private val cachedErrorMessages: ErrorCache = mutableMapOf() /** * An unknown error occurred. */ -internal class UnknownErrorMessage( - override val cause: AuthException -) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_unknown), AuthenticatorMessage.Error +internal class UnknownErrorMessage(override val cause: AuthException) : + AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_unknown), + AuthenticatorMessage.Error { + + override fun message(context: Context): String { + return message(context, cachedErrorMessages) + } + + @SuppressLint("DiscouragedApi") + internal fun message(context: Context, cache: ErrorCache): String { + return cache.getOrPut(cause::class) { + // Check if the customer application has defined a specific string for this Exception type. If not, return + // the generic error message. + val resourceName = cause.toResourceName() + val resourceId = context.resources.getIdentifier(resourceName, "string", context.packageName) + if (resourceId != 0) context.getString(resourceId) else super.message(context) + } + } +} /** * The username or password were incorrect. */ -internal class InvalidLoginMessage( - override val cause: AuthException -) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_invalid_signin), AuthenticatorMessage.Error +internal class InvalidLoginMessage(override val cause: AuthException) : + AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_invalid_signin), + AuthenticatorMessage.Error /** * The server could not send a confirmation code to the user. */ -internal class CannotSendCodeMessage( - override val cause: AuthException -) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_send_code), AuthenticatorMessage.Error +internal class CannotSendCodeMessage(override val cause: AuthException) : + AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_send_code), + AuthenticatorMessage.Error /** * The entered confirmation code has expired. */ -internal class ExpiredCodeMessage( - override val cause: AuthException -) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_expired_code), AuthenticatorMessage.Error +internal class ExpiredCodeMessage(override val cause: AuthException) : + AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_expired_code), + AuthenticatorMessage.Error /** * The device may not have connectivity. */ -internal class NetworkErrorMessage( - override val cause: AuthException -) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_network), AuthenticatorMessage.Error +internal class NetworkErrorMessage(override val cause: AuthException) : + AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_network), + AuthenticatorMessage.Error /** * User tried an action too many times. */ -internal class LimitExceededMessage( - override val cause: AuthException -) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_limit_exceeded), AuthenticatorMessage.Error +internal class LimitExceededMessage(override val cause: AuthException) : + AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_limit_exceeded), + AuthenticatorMessage.Error diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/Exceptions.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/Exceptions.kt index 429e6d2c..4879f979 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/Exceptions.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/Exceptions.kt @@ -16,27 +16,42 @@ package com.amplifyframework.ui.authenticator.util import com.amplifyframework.auth.AuthException +import com.amplifyframework.auth.result.step.AuthSignInStep import java.net.UnknownHostException /** * Exception that is passed to the errorContent if the application does not have the auth plugin * configured when attempting to use Authenticator */ -class MissingConfigurationException : AuthException( - "Missing auth configuration", - "Make sure the Auth plugin is added and Amplify.configure is called. See " + - "https://docs.amplify.aws/lib/auth/getting-started/q/platform/android/ for details" -) +class MissingConfigurationException : + AuthException( + "Missing auth configuration", + "Make sure the Auth plugin is added and Amplify.configure is called. See " + + "https://docs.amplify.aws/lib/auth/getting-started/q/platform/android/ for details" + ) /** * Exception that is passed to the errorContent if the configuration passed to the auth plugin is missing a required * property or has an invalid property */ -class InvalidConfigurationException(message: String, cause: Exception?) : AuthException( - message = message, - recoverySuggestion = "Check that the configuration passed to Amplify.configure has all required fields", - cause = cause -) +class InvalidConfigurationException(message: String, cause: Exception?) : + AuthException( + message = message, + recoverySuggestion = "Check that the configuration passed to Amplify.configure has all required fields", + cause = cause + ) + +/** + * Exception that occurs if Amplify returns a "nextStep" that is not supported by Authenticator. This might happen + * if Amplify has released a new feature and support is still outstanding in Authenticator. + */ +class UnsupportedNextStepException internal constructor(nextStep: AuthSignInStep) : + AuthException( + message = "Unsupported next step $nextStep.", + recoverySuggestion = + "Authenticator does not support this Authentication flow, disable it to use this version of " + + "Authenticator, or check if support has been added in a new version." + ) internal fun Throwable.isConnectivityIssue(): Boolean { if (this is UnknownHostException) { @@ -47,3 +62,9 @@ internal fun Throwable.isConnectivityIssue(): Boolean { else -> cause.isConnectivityIssue() } } + +private val camelRegex = "(?<=[a-zA-Z])[A-Z]".toRegex() +private fun String.toSnakeCase() = camelRegex.replace(this) { "_${it.value}" }.lowercase() + +internal fun AuthException.toResourceName() = + "amplify_ui_authenticator_error_" + this::class.simpleName?.removeSuffix("Exception")?.toSnakeCase() diff --git a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/util/AuthenticatorMessageTest.kt b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/util/AuthenticatorMessageTest.kt new file mode 100644 index 00000000..17fd9b54 --- /dev/null +++ b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/util/AuthenticatorMessageTest.kt @@ -0,0 +1,66 @@ +package com.amplifyframework.ui.authenticator.util + +import android.content.Context +import com.amplifyframework.auth.AuthException +import com.amplifyframework.ui.authenticator.R +import io.kotest.matchers.maps.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlin.reflect.KClass +import org.junit.Test + +class AuthenticatorMessageTest { + + @Test + fun `unknown error message returns resource string if defined`() { + val context = mockk { + every { packageName } returns "package" + every { + resources.getIdentifier("amplify_ui_authenticator_error_missing_configuration", "string", "package") + } returns 42 + every { getString(42) } returns "override string" + } + + val message = UnknownErrorMessage(MissingConfigurationException()) + message.message(context, mutableMapOf()) shouldBe "override string" + } + + @Test + fun `unknown error message caches resource string if defined`() { + val cache = mutableMapOf, String>() + val context = mockk { + every { packageName } returns "package" + every { + resources.getIdentifier("amplify_ui_authenticator_error_missing_configuration", "string", "package") + } returns 42 + every { getString(42) } returns "override string" + } + + // Call multiple times + val message = UnknownErrorMessage(MissingConfigurationException()) + message.message(context, cache) shouldBe "override string" + cache.shouldHaveSize(1) + message.message(context, cache) shouldBe "override string" + + // Resource should have only be read once + verify(exactly = 1) { + context.getString(42) + } + } + + @Test + fun `unknown error message returns default message if resource string not defined`() { + val context = mockk { + every { packageName } returns "package" + every { + resources.getIdentifier("amplify_ui_authenticator_error_missing_configuration", "string", "package") + } returns 0 + every { getString(R.string.amplify_ui_authenticator_error_unknown) } returns "default string" + } + + val message = UnknownErrorMessage(MissingConfigurationException()) + message.message(context, mutableMapOf()) shouldBe "default string" + } +} diff --git a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/util/ExceptionsTest.kt b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/util/ExceptionsTest.kt new file mode 100644 index 00000000..6df223b8 --- /dev/null +++ b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/util/ExceptionsTest.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2025 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.ui.authenticator.util + +import io.kotest.matchers.shouldBe +import org.junit.Test + +class ExceptionsTest { + + @Test + fun `InvalidConfigurationException maps to the expected resource name`() { + val exception = InvalidConfigurationException("test", null) + exception.toResourceName() shouldBe "amplify_ui_authenticator_error_invalid_configuration" + } +}