// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.credentials.metadataservice

import com.github.tomakehurst.wiremock.client.WireMock.aResponse
import com.github.tomakehurst.wiremock.client.WireMock.any
import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo
import com.github.tomakehurst.wiremock.core.WireMockConfiguration
import com.github.tomakehurst.wiremock.junit.WireMockRule
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ProjectRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
import software.amazon.awssdk.core.SdkSystemSetting
import software.aws.toolkits.core.credentials.CredentialType
import software.aws.toolkits.core.credentials.CredentialsChangeEvent
import software.aws.toolkits.core.credentials.CredentialsChangeListener
import software.aws.toolkits.core.rules.SystemPropertyHelper
import java.time.Duration
import java.time.Instant
import java.time.format.DateTimeFormatter

class ContainerCredentialProviderFactoryTest {
    @Rule
    @JvmField
    val projectRule = ProjectRule()

    @Rule
    @JvmField
    val systemPropertyHelper = SystemPropertyHelper()

    @Rule
    @JvmField
    val wireMockRule = WireMockRule(
        WireMockConfiguration.wireMockConfig()
            .dynamicPort()
    )

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    private val profileLoadCallback = mock<CredentialsChangeListener>()
    private val credentialChangeEvent = argumentCaptor<CredentialsChangeEvent>()
    private val sut = ContainerCredentialProviderFactory()

    @Before
    fun setUp() {
        System.setProperty(SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI.property(), "")
        System.setProperty(SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_FULL_URI.property(), "")
        profileLoadCallback.stub {
            on { profileLoadCallback.invoke(credentialChangeEvent.capture()) }.thenReturn(Unit)
        }
    }

    @Test
    fun `provides credentials when AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is set`() {
        System.setProperty(SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI.property(), "http://localhost:${wireMockRule.port()}")
        sut.setUp(profileLoadCallback)

        assertThat(credentialChangeEvent.allValues.size).isEqualTo(1)
        assertThat(credentialChangeEvent.firstValue).satisfies {
            assertThat(it.modified).isEmpty()
            assertThat(it.removed).isEmpty()
            assertThat(it.added).hasSize(1)
            assertThat(it.added.first().credentialType).isEqualTo(CredentialType.EcsMetadata)
        }
    }

    @Test
    fun `provides credentials when AWS_CONTAINER_CREDENTIALS_FULL_URI is set`() {
        System.setProperty(SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_FULL_URI.property(), "http://localhost:${wireMockRule.port()}")
        sut.setUp(profileLoadCallback)

        assertThat(credentialChangeEvent.allValues.size).isEqualTo(1)
        assertThat(credentialChangeEvent.firstValue).satisfies {
            assertThat(it.modified).isEmpty()
            assertThat(it.removed).isEmpty()
            assertThat(it.added).hasSize(1)
            assertThat(it.added.first().credentialType).isEqualTo(CredentialType.EcsMetadata)
        }
    }

    @Test
    fun `credentials can be resolved`() {
        // assume sdk handles full_uri/relative_uri properly for us
        System.setProperty(SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_FULL_URI.property(), "http://localhost:${wireMockRule.port()}")
        System.setProperty(SdkSystemSetting.AWS_CONTAINER_SERVICE_ENDPOINT.property(), "http://localhost:${wireMockRule.port()}")
        wireMockRule.stubFor(
            any(urlPathEqualTo("/"))
                .willReturn(
                    aResponse().withBody(
                        // language=JSON
                        """
                            {
                                "AccessKeyId": "accessKeyId",
                                "SecretAccessKey": "secretAccessKey",
                                "Token": "sessionToken",
                                "Expiration": "${DateTimeFormatter.ISO_INSTANT.format(Instant.now().plus(Duration.ofDays(1)))}"
                            }
                        """.trimIndent()
                    )
                )
        )
        val provider = sut.createAwsCredentialProvider(mock(), mock())
        val credentials = provider.resolveCredentials() as AwsSessionCredentials

        assertThat(credentials.accessKeyId()).isEqualTo("accessKeyId")
        assertThat(credentials.secretAccessKey()).isEqualTo("secretAccessKey")
        assertThat(credentials.sessionToken()).isEqualTo("sessionToken")
    }

    @Test
    fun `does not provide credentials when container variables are not set`() {
        sut.setUp(profileLoadCallback)

        assertThat(credentialChangeEvent.allValues).isEmpty()
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.credentials.metadataservice

import com.github.tomakehurst.wiremock.client.WireMock.aResponse
import com.github.tomakehurst.wiremock.client.WireMock.any
import com.github.tomakehurst.wiremock.client.WireMock.head
import com.github.tomakehurst.wiremock.client.WireMock.put
import com.github.tomakehurst.wiremock.client.WireMock.status
import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
import com.github.tomakehurst.wiremock.junit.WireMockRule
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ProjectRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
import software.amazon.awssdk.core.SdkSystemSetting
import software.aws.toolkits.core.credentials.CredentialType
import software.aws.toolkits.core.credentials.CredentialsChangeEvent
import software.aws.toolkits.core.credentials.CredentialsChangeListener
import software.aws.toolkits.core.rules.SystemPropertyHelper
import java.time.Duration
import java.time.Instant
import java.time.format.DateTimeFormatter

class InstanceRoleCredentialProviderFactoryTest {
    @Rule
    @JvmField
    val projectRule = ProjectRule()

    @Rule
    @JvmField
    val systemPropertyHelper = SystemPropertyHelper()

    @Rule
    @JvmField
    val wireMockRule = WireMockRule(
        wireMockConfig()
            .dynamicPort()
    )

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    private val profileLoadCallback = mock<CredentialsChangeListener>()
    private val credentialChangeEvent = argumentCaptor<CredentialsChangeEvent>()
    private val sut = InstanceRoleCredentialProviderFactory()

    @Before
    fun setUp() {
        System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "http://localhost:${wireMockRule.port()}")

        profileLoadCallback.stub {
            on { profileLoadCallback.invoke(credentialChangeEvent.capture()) }.thenReturn(Unit)
        }

        wireMockRule.stubFor(head(urlPathEqualTo("/")).willReturn(status(200)))
        wireMockRule.stubFor(put(urlPathEqualTo("/latest/api/token")).willReturn(status(200)))
        wireMockRule.stubFor(
            any(urlPathEqualTo("/latest/meta-data/iam/security-credentials/"))
                .willReturn(
                    aResponse().withBody("aProfile")
                )
        )
        wireMockRule.stubFor(
            any(urlPathEqualTo("/latest/meta-data/iam/security-credentials/aProfile"))
                .willReturn(
                    aResponse().withBody(
                        // language=JSON
                        """
                            {
                                "AccessKeyId": "accessKeyId",
                                "SecretAccessKey": "secretAccessKey",
                                "Token": "sessionToken",
                                "Expiration": "${DateTimeFormatter.ISO_INSTANT.format(Instant.now().plus(Duration.ofDays(1)))}"
                            }
                        """.trimIndent()
                    )
                )
        )
        wireMockRule.stubFor(
            any(urlPathEqualTo("/latest/dynamic/instance-identity/document"))
                .willReturn(
                    aResponse().withBody("""{"region": "us-fake-1"}""")
                )
        )
    }

    @Test
    fun `provides credentials when ec2 metadata service is available`() {
        sut.setUp(profileLoadCallback)

        assertThat(credentialChangeEvent.allValues.size).isEqualTo(1)
        assertThat(credentialChangeEvent.firstValue).satisfies {
            assertThat(it.modified).isEmpty()
            assertThat(it.removed).isEmpty()
            assertThat(it.added).hasSize(1)
            assertThat(it.added.first().credentialType).isEqualTo(CredentialType.Ec2Metadata)
        }
    }

    @Test
    fun `credentials can be resolved`() {
        val provider = sut.createAwsCredentialProvider(mock(), mock())
        val credentials = provider.resolveCredentials() as AwsSessionCredentials

        assertThat(credentials.accessKeyId()).isEqualTo("accessKeyId")
        assertThat(credentials.secretAccessKey()).isEqualTo("secretAccessKey")
        assertThat(credentials.sessionToken()).isEqualTo("sessionToken")
    }

    @Test
    fun `does not provide credentials when ec2 metadata service property is empty`() {
        System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "")
        sut.setUp(profileLoadCallback)

        assertThat(credentialChangeEvent.allValues).isEmpty()
    }

    @Test
    fun `does not provide credentials when ec2 metadata service is unreachable`() {
        System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "http://localhost:0")
        sut.setUp(profileLoadCallback)

        assertThat(credentialChangeEvent.allValues).isEmpty()
    }

    @Test
    fun `does not provide credentials when system property disables sdk resolver`() {
        System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.property(), "true")
        sut.setUp(profileLoadCallback)

        assertThat(credentialChangeEvent.allValues).isEmpty()
    }
}


// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.credentials.pinning

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.ui.TestDialog
import com.intellij.openapi.ui.TestDialogManager
import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.replaceService
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.whenever
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
import software.aws.toolkits.jetbrains.core.credentials.AwsCredentialConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitAuthManager
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection

class ConnectionPinningManagerTest {

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    @Rule
    @JvmField
    val applicationRule = ApplicationRule()

    private lateinit var sut: DefaultConnectionPinningManager

    @Before
    fun setUp() {
        sut = spy(DefaultConnectionPinningManager())
    }

    @Test
    fun `switching connection to unsupported feature pins connection to initial connection if user allows`() {
        sut.stub {
            onGeneric { it.showDialogIfNeeded(any(), any(), any(), any()) } doReturn true
        }
        TestDialogManager.setTestDialog(TestDialog.OK)

        val feature = object : FeatureWithPinnedConnection {
            override val featureId = "mockId"
            override val featureName = "mockFeature"
            override fun supportsConnectionType(connection: ToolkitConnection) = true
        }

        val oldConnection = mock<AwsBearerTokenConnection> {
            on { id } doReturn "connId"
        }
        val mockAuthManager = mock<ToolkitAuthManager> {
            whenever(it.getConnection(any())).thenReturn(oldConnection)
        }
        ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, mockAuthManager, disposableRule.disposable)

        sut.maybePinFeatures(oldConnection, mock<AwsBearerTokenConnection>(), listOf(feature))

        assertThat(sut.getPinnedConnection(feature)).isEqualTo(oldConnection)
    }

    @Test
    fun `add new supported connection will pin the feature if user allows`() {
        sut.stub {
            onGeneric { it.showDialogIfNeeded(any(), any(), any(), any()) } doReturn true
        }
        TestDialogManager.setTestDialog(TestDialog.OK)

        val feature = object : FeatureWithPinnedConnection {
            override val featureId = "mockId"
            override val featureName = "mockFeature"
            override fun supportsConnectionType(connection: ToolkitConnection) = true
        }

        val newConnection = mock<AwsBearerTokenConnection> {
            on { id } doReturn "connId"
        }

        val mockAuthManager = mock<ToolkitAuthManager> {
            on { it.getConnection("connId") }.thenReturn(newConnection)
        }
        ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, mockAuthManager, disposableRule.disposable)

        sut.maybePinFeatures(null, newConnection, listOf(feature))

        assertThat(sut.getPinnedConnection(feature)).isEqualTo(newConnection)
    }

    @Test
    fun `switching connection from unsupported feature pins connection to new connection if user allows`() {
        sut.stub {
            onGeneric { it.showDialogIfNeeded(any(), any(), any(), any()) } doReturn true
        }
        TestDialogManager.setTestDialog(TestDialog.OK)

        val oldConnectionId = "connId"

        val feature = object : FeatureWithPinnedConnection {
            override val featureId = "mockId"
            override val featureName = "mockFeature"
            override fun supportsConnectionType(connection: ToolkitConnection) =
                connection is AwsBearerTokenConnection
        }

        val oldConnection = mock<AwsCredentialConnection> {
            on { id } doReturn oldConnectionId
        }

        val newConnection = mock<AwsBearerTokenConnection> {
            on { id } doReturn "newId"
        }
        val mockAuthManager = mock<ToolkitAuthManager> {
            whenever(it.getConnection(any())).thenReturn(oldConnection)
        }
        ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, mockAuthManager, disposableRule.disposable)

        sut.maybePinFeatures(oldConnection, newConnection, listOf(feature))

        assertThat(sut.getPinnedConnection(feature)).isEqualTo(newConnection)
    }

    @Test
    fun `switching connection to unsupported feature does not pin connection to initial if user declines`() {
        sut.stub {
            onGeneric { it.showDialogIfNeeded(any(), any(), any(), any()) } doReturn false
        }
        TestDialogManager.setTestDialog(TestDialog.NO)

        val feature = object : FeatureWithPinnedConnection {
            override val featureId = "mockId"
            override val featureName = "mockFeature"
            override fun supportsConnectionType(connection: ToolkitConnection) = true
        }

        val oldConnection = mock<AwsBearerTokenConnection> {
            on { id } doReturn "connId"
        }
        val mockAuthManager = mock<ToolkitAuthManager> {
            whenever(it.getConnection(any())).thenReturn(oldConnection)
        }
        ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, mockAuthManager, disposableRule.disposable)

        sut.maybePinFeatures(oldConnection, mock<AwsBearerTokenConnection>(), listOf(feature))

        assertThat(sut.getPinnedConnection(feature)).isNull()
    }

    @Test
    fun `pinned connection returns null if connection no longer exists in auth manager`() {
        val feature = object : FeatureWithPinnedConnection {
            override val featureId = "mockId"
            override val featureName = "mockFeature"
            override fun supportsConnectionType(connection: ToolkitConnection) = true
        }
        val connection = mock<AwsBearerTokenConnection> {
            on { id } doReturn "connId"
        }

        sut.setPinnedConnection(feature, connection)

        assertThat(sut.getPinnedConnection(feature)).isNull()
    }

    @Test
    fun `respects pinned feature`() {
        val feature = object : FeatureWithPinnedConnection {
            override val featureId = "mockId"
            override val featureName = "mockFeature"
            override fun supportsConnectionType(connection: ToolkitConnection) = true
        }
        val connection = mock<AwsBearerTokenConnection> {
            on { id } doReturn "connId"
        }
        val mockAuthManager = mock<ToolkitAuthManager> {
            whenever(it.getConnection(any())).thenReturn(connection)
        }
        ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, mockAuthManager, disposableRule.disposable)

        sut.setPinnedConnection(feature, connection)

        assertThat(sut.getPinnedConnection(feature)).isEqualTo(connection)
    }

    @Test
    fun `respects pinning prompt = yes`() {
        val connection = mock<AwsBearerTokenConnection> {
            on { id } doReturn "connId"
        }
        val dialogMock = mock<TestDialog>()
        TestDialogManager.setTestDialog(dialogMock)

        sut.shouldPinConnections = true
        assertThat(sut.showDialogIfNeeded(connection, connection, "feature")).isTrue()
        verifyNoInteractions(dialogMock)
    }

    @Test
    fun `respects pinning prompt = no`() {
        val connection = mock<AwsBearerTokenConnection> {
            on { id } doReturn "connId"
        }
        val dialogMock = mock<TestDialog>()
        TestDialogManager.setTestDialog(dialogMock)

        sut.shouldPinConnections = false
        assertThat(sut.showDialogIfNeeded(connection, connection, "feature")).isFalse()
        verifyNoInteractions(dialogMock)
    }

    @Test
    fun `prompts for pinning`() {
        val connection = mock<AwsBearerTokenConnection> {
            on { id } doReturn "connId"
        }

        sut.shouldPinConnections = null
        TestDialogManager.setTestDialog(TestDialog.YES)
        assertThat(sut.showDialogIfNeeded(connection, connection, "feature")).isTrue()
        TestDialogManager.setTestDialog(TestDialog.NO)
        assertThat(sut.showDialogIfNeeded(connection, connection, "feature")).isFalse()
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.credentials.profiles

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import software.aws.toolkits.core.rules.EnvironmentVariableHelper
import software.aws.toolkits.core.rules.SystemPropertyHelper
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.jetbrains.core.credentials.profiles.Ec2MetadataConfigProvider.getEc2MedataEndpoint
import software.aws.toolkits.jetbrains.utils.isInstanceOf

class Ec2MetadataConfigProviderTest {
    @Rule
    @JvmField
    val sysProps = SystemPropertyHelper()

    @Rule
    @JvmField
    val envVars = EnvironmentVariableHelper()

    @Test
    fun `endpoint can be overridden with system property`() {
        val endpoint = aString()
        System.setProperty("aws.ec2MetadataServiceEndpoint", endpoint)

        assertThat(profile().getEc2MedataEndpoint()).isEqualTo(endpoint)
    }

    @Test
    fun `endpoint can be overridden with env var`() {
        val endpoint = aString()
        envVars["AWS_EC2_METADATA_SERVICE_ENDPOINT"] = endpoint

        assertThat(profile().getEc2MedataEndpoint()).isEqualTo(endpoint)
    }

    @Test
    fun `endpoint can be overridden with profile`() {
        val endpoint = aString()
        val profile = profile {
            put("ec2_metadata_service_endpoint", endpoint)
        }

        assertThat(profile.getEc2MedataEndpoint()).isEqualTo(endpoint)
    }

    @Test
    fun `endpoint defaults to ipv4 endpoint if nothing is specified`() {
        assertThat(profile().getEc2MedataEndpoint()).isEqualTo("http://169.254.169.254")
    }

    @Test
    fun `endpoint defaults to default endpoint if ipv6 is specified`() {
        val profile = profile {
            put("ec2_metadata_service_endpoint_mode", "ipv6")
        }

        assertThat(profile.getEc2MedataEndpoint()).isEqualTo("http://[fd00:ec2::254]")
    }

    @Test
    fun `mode is case insensitive`() {
        val profile = profile {
            put("ec2_metadata_service_endpoint_mode", "ipv6")
        }

        assertThat(profile.getEc2MedataEndpoint()).isEqualTo("http://[fd00:ec2::254]")

        val profile2 = profile {
            put("ec2_metadata_service_endpoint_mode", "iPv6")
        }

        assertThat(profile2.getEc2MedataEndpoint()).isEqualTo("http://[fd00:ec2::254]")
    }

    @Test
    fun `mode can be overridden with system property`() {
        System.setProperty("aws.ec2MetadataServiceEndpointMode", "ipv6")

        assertThat(profile().getEc2MedataEndpoint()).isEqualTo("http://[fd00:ec2::254]")
    }

    @Test
    fun `mode can be overridden with env var`() {
        envVars["AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE"] = "ipv6"

        assertThat(profile().getEc2MedataEndpoint()).isEqualTo("http://[fd00:ec2::254]")
    }

    @Test
    fun `invalid mode fails`() {
        val profile = profile {
            put("ec2_metadata_service_endpoint_mode", "badMode")
        }

        assertThatThrownBy { profile.getEc2MedataEndpoint() }.isInstanceOf<IllegalArgumentException>()
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.credentials.profiles

import com.intellij.openapi.ui.TestDialogManager
import com.intellij.openapi.ui.TestInputDialog
import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.RuleChain
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.profiles.ProfileProperty
import software.amazon.awssdk.services.sts.StsClient
import software.amazon.awssdk.services.sts.model.AssumeRoleRequest
import software.amazon.awssdk.services.sts.model.AssumeRoleResponse
import software.amazon.awssdk.utils.SdkAutoCloseable
import software.aws.toolkits.core.region.anAwsRegion
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.jetbrains.core.MockClientManagerRule
import java.time.Duration
import java.time.Instant

class ProfileAssumeRoleProviderTest {
    private val application = ApplicationRule()
    private val clientManager = MockClientManagerRule()

    @Rule
    @JvmField
    val ruleChain = RuleChain(application, clientManager)

    private val mfaToken = "SomeToken"
    private lateinit var parentProvider: AwsCredentialsProvider
    private lateinit var stsClient: StsClient

    @Before
    fun setup() {
        parentProvider = mock(extraInterfaces = arrayOf(SdkAutoCloseable::class))

        stsClient = clientManager.create<StsClient>().stub {
            on { assumeRole(any<AssumeRoleRequest>()) } doReturn AssumeRoleResponse.builder()
                .credentials { c ->
                    c.accessKeyId(aString())
                    c.secretAccessKey(aString())
                    c.sessionToken(aString())
                    c.expiration(Instant.now().plus(Duration.ofHours(1)))
                }.build()
        }

        TestDialogManager.setTestInputDialog { mfaToken }
    }

    @After
    fun tearDown() {
        TestDialogManager.setTestInputDialog(TestInputDialog.DEFAULT)
    }

    @Test
    fun `role_arn gets passed`() {
        val role = aString()
        val profile = profile {
            put(ProfileProperty.ROLE_ARN, role)
        }

        ProfileAssumeRoleProvider(parentProvider, anAwsRegion(), profile).resolveCredentials()

        argumentCaptor<AssumeRoleRequest> {
            verify(stsClient).assumeRole(capture())

            assertThat(firstValue.roleArn()).isEqualTo(role)
        }
    }

    @Test
    fun `duration_seconds gets respected if provided`() {
        val profile = profile {
            put(ProfileProperty.ROLE_ARN, aString())
            put(ProfileProperty.DURATION_SECONDS, "12345")
        }

        ProfileAssumeRoleProvider(parentProvider, anAwsRegion(), profile).resolveCredentials()

        argumentCaptor<AssumeRoleRequest> {
            verify(stsClient).assumeRole(capture())

            assertThat(firstValue.durationSeconds()).isEqualTo(12345)
        }
    }

    @Test
    fun `duration_seconds uses default if not provided`() {
        val profile = profile {
            put(ProfileProperty.ROLE_ARN, aString())
            put(ProfileProperty.DURATION_SECONDS, "abc")
        }

        ProfileAssumeRoleProvider(parentProvider, anAwsRegion(), profile).resolveCredentials()

        argumentCaptor<AssumeRoleRequest> {
            verify(stsClient).assumeRole(capture())

            assertThat(firstValue.durationSeconds()).isEqualTo(3600)
        }
    }

    @Test
    fun `duration_seconds uses default if invalid format`() {
        val profile = profile {
            put(ProfileProperty.ROLE_ARN, aString())
        }

        ProfileAssumeRoleProvider(parentProvider, anAwsRegion(), profile).resolveCredentials()

        argumentCaptor<AssumeRoleRequest> {
            verify(stsClient).assumeRole(capture())

            assertThat(firstValue.durationSeconds()).isEqualTo(3600)
        }
    }

    @Test
    fun `MFA is prompted if keys are specified`() {
        val mfaSerial = aString()
        val profile = profile {
            put(ProfileProperty.ROLE_ARN, aString())
            put(ProfileProperty.MFA_SERIAL, mfaSerial)
        }

        ProfileAssumeRoleProvider(parentProvider, anAwsRegion(), profile).resolveCredentials()

        argumentCaptor<AssumeRoleRequest> {
            verify(stsClient).assumeRole(capture())

            assertThat(firstValue.tokenCode()).isEqualTo(mfaToken)
        }
    }

    @Test
    fun `external ID is respected if provided`() {
        val id = aString()
        val profile = profile {
            put(ProfileProperty.ROLE_ARN, aString())
            put(ProfileProperty.EXTERNAL_ID, id)
        }

        ProfileAssumeRoleProvider(parentProvider, anAwsRegion(), profile).resolveCredentials()

        argumentCaptor<AssumeRoleRequest> {
            verify(stsClient).assumeRole(capture())

            assertThat(firstValue.externalId()).isEqualTo(id)
        }
    }

    @Test
    fun `role session name is respected if provided`() {
        val name = aString()
        val profile = profile {
            put(ProfileProperty.ROLE_ARN, aString())
            put(ProfileProperty.ROLE_SESSION_NAME, name)
        }

        ProfileAssumeRoleProvider(parentProvider, anAwsRegion(), profile).resolveCredentials()

        argumentCaptor<AssumeRoleRequest> {
            verify(stsClient).assumeRole(capture())

            assertThat(firstValue.roleSessionName()).isEqualTo(name)
        }
    }

    @Test
    fun `calling close shuts down parent provider and client`() {
        val profile = profile {
            put(ProfileProperty.ROLE_ARN, aString())
        }

        ProfileAssumeRoleProvider(parentProvider, anAwsRegion(), profile).close()

        verify(stsClient).close()
        verify(parentProvider as SdkAutoCloseable).close()
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.credentials.profiles

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.ui.TestDialogManager
import com.intellij.openapi.ui.TestInputDialog
import com.intellij.openapi.util.io.FileUtil
import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.RuleChain
import com.intellij.testFramework.replaceService
import org.assertj.core.api.Assertions.STRING
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.assertj.core.api.Condition
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.reset
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
import software.amazon.awssdk.auth.credentials.ContainerCredentialsProvider
import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.services.sso.SsoClient
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
import software.amazon.awssdk.services.sts.StsClient
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.core.credentials.CredentialsChangeEvent
import software.aws.toolkits.core.credentials.CredentialsChangeListener
import software.aws.toolkits.core.rules.SystemPropertyHelper
import software.aws.toolkits.jetbrains.core.MockClientManagerRule
import software.aws.toolkits.jetbrains.core.credentials.InteractiveCredential
import software.aws.toolkits.jetbrains.core.credentials.ToolkitCredentialProcessProvider
import software.aws.toolkits.jetbrains.core.credentials.sso.SsoCache
import software.aws.toolkits.jetbrains.core.region.getDefaultRegion
import software.aws.toolkits.jetbrains.utils.isInstanceOf
import software.aws.toolkits.jetbrains.utils.isInstanceOfSatisfying
import software.aws.toolkits.jetbrains.utils.rules.NotificationListenerRule
import java.io.File
import java.util.function.Function

class ProfileCredentialProviderFactoryTest {
    private val temporaryFolder = TemporaryFolder()
    private val systemPropertyHelper = SystemPropertyHelper()
    private val disposableRule = DisposableRule()
    private val notificationListener = NotificationListenerRule(disposableRule.disposable)
    private val clientManager = MockClientManagerRule()

    @Rule
    @JvmField
    val ruleChain = RuleChain(
        temporaryFolder,
        ApplicationRule(),
        systemPropertyHelper,
        notificationListener,
        disposableRule,
        clientManager
    )

    private lateinit var profileFile: File

    private val mockProfileWatcher = MockProfileWatcher()
    private val profileLoadCallback = mock<CredentialsChangeListener>()
    private val credentialChangeEvent = argumentCaptor<CredentialsChangeEvent>()

    @Before
    fun setUp() {
        reset(profileLoadCallback)

        val awsFolder = temporaryFolder.newFolder(".aws")
        profileFile = File(awsFolder, "config")

        System.getProperties().setProperty("aws.configFile", profileFile.absolutePath)
        System.getProperties().setProperty("aws.sharedCredentialsFile", File(awsFolder, "credentials").absolutePath)

        profileLoadCallback.stub {
            on { profileLoadCallback.invoke(credentialChangeEvent.capture()) }.thenReturn(Unit)
        }

        ApplicationManager.getApplication().replaceService(ProfileWatcher::class.java, mockProfileWatcher, disposableRule.disposable)

        TestDialogManager.setTestInputDialog { MFA_TOKEN }
    }

    @After
    fun tearDown() {
        TestDialogManager.setTestInputDialog(TestInputDialog.DEFAULT)
        mockProfileWatcher.reset()
    }

    @Test
    fun `empty profiles report nothing found`() {
        createProviderFactory()

        argumentCaptor<CredentialsChangeEvent> {
            verify(profileLoadCallback).invoke(capture())

            assertThat(firstValue.added).isEmpty()
            assertThat(firstValue.modified).isEmpty()
            assertThat(firstValue.removed).isEmpty()
        }
    }

    @Test
    fun `loading supported profiles are reported as added`() {
        writeProfileFile(
            """
            [profile noRegion]
            aws_access_key_id=BarAccessKey
            aws_secret_access_key=BarSecretKey

            [profile regionalized]
            aws_access_key_id=RegionAccessKey
            aws_secret_access_key=RegionSecretKey
            region=us-west-2
            """.trimIndent()
        )

        createProviderFactory()

        argumentCaptor<CredentialsChangeEvent> {
            verify(profileLoadCallback).invoke(capture())

            assertThat(firstValue.added).hasSize(2)
                .has(profileName("noRegion"))
                .has(profileName("regionalized", defaultRegion = "us-west-2"))

            assertThat(firstValue.modified).isEmpty()
            assertThat(firstValue.removed).isEmpty()
        }
    }

    @Test
    fun `failing to load a profile shows a notification`() {
        writeProfileFile(
            """
            [profile bar]
            aws_access_key_id
            """.trimIndent()
        )

        createProviderFactory()

        assertThat(notificationListener.notifications)
            .extracting(Function { t -> t.content })
            .singleElement(STRING)
            .contains("Expected an '=' sign defining a property on line 2")
    }

    @Test
    fun `an error does not prevent loading of other profiles and is reported`() {
        writeProfileFile(
            """
            [profile role]
            role_arn=arn1
            role_session_name=testSession
            external_id=externalId
            source_profile=doNotExist

            [profile another_profile]
            aws_access_key_id=BarAccessKey
            aws_secret_access_key=BarSecretKey
            """.trimIndent()
        )

        createProviderFactory()

        argumentCaptor<CredentialsChangeEvent> {
            verify(profileLoadCallback).invoke(capture())

            assertThat(firstValue.added).hasSize(1).has(profileName("another_profile"))
            assertThat(firstValue.modified).isEmpty()
            assertThat(firstValue.removed).isEmpty()
        }

        assertThat(notificationListener.notifications)
            .extracting(Function { t -> t.content })
            .singleElement(STRING)
            .contains("2 profiles found. Failed to load 1 profile.")
    }

    @Test
    fun `modifying a profile gets reported as a modification`() {
        writeProfileFile(
            """
            [profile foo]
            aws_access_key_id=FooAccessKey
            aws_secret_access_key=FooSecretKey
            aws_session_token=FooSessionToken
            """.trimIndent()
        )

        createProviderFactory()

        writeProfileFile(
            """
            [profile foo]
            aws_access_key_id=FooAccessKey2
            aws_secret_access_key=FooSecretKey2
            aws_session_token=FooSessionToken2
            """.trimIndent()
        )

        mockProfileWatcher.triggerListeners()

        argumentCaptor<CredentialsChangeEvent> {
            verify(profileLoadCallback, times(2)).invoke(capture())

            assertThat(firstValue.added).hasSize(1).has(profileName("foo"))
            assertThat(firstValue.modified).isEmpty()
            assertThat(firstValue.removed).isEmpty()

            assertThat(secondValue.added).isEmpty()
            assertThat(secondValue.modified).hasSize(1).has(profileName("foo"))
            assertThat(secondValue.removed).isEmpty()
        }
    }

    @Test
    fun `deleting a profile gets reported as a deletion`() {
        writeProfileFile(
            """
            [profile foo]
            aws_access_key_id=FooAccessKey
            aws_secret_access_key=FooSecretKey
            aws_session_token=FooSessionToken
            """.trimIndent()
        )

        createProviderFactory()

        writeProfileFile("")

        mockProfileWatcher.triggerListeners()

        argumentCaptor<CredentialsChangeEvent> {
            verify(profileLoadCallback, times(2)).invoke(capture())

            assertThat(firstValue.added).hasSize(1).has(profileName("foo"))
            assertThat(firstValue.modified).isEmpty()
            assertThat(firstValue.removed).isEmpty()

            assertThat(secondValue.added).isEmpty()
            assertThat(secondValue.modified).isEmpty()
            assertThat(secondValue.removed).hasSize(1).has(profileName("foo"))
        }
    }

    @Test
    fun `adding a profile gets reported as an addition`() {
        writeProfileFile("")

        createProviderFactory()

        writeProfileFile(
            """
            [profile foo]
            aws_access_key_id=FooAccessKey
            aws_secret_access_key=FooSecretKey
            aws_session_token=FooSessionToken
            """.trimIndent()
        )

        mockProfileWatcher.triggerListeners()

        argumentCaptor<CredentialsChangeEvent> {
            verify(profileLoadCallback, times(2)).invoke(capture())

            assertThat(firstValue.added).isEmpty()
            assertThat(firstValue.modified).isEmpty()
            assertThat(firstValue.removed).isEmpty()

            assertThat(secondValue.added).hasSize(1).has(profileName("foo"))
            assertThat(secondValue.modified).isEmpty()
            assertThat(secondValue.removed).isEmpty()
        }
    }

    @Test
    fun `modifying a parent credential provider reports modification for it and its children`() {
        writeProfileFile(
            """
            [profile foo]
            aws_access_key_id=FooAccessKey
            aws_secret_access_key=FooSecretKey
            aws_session_token=FooSessionToken

            [profile bar]
            source_profile=foo
            role_arn=SomeArn

            [profile baz]
            source_profile=bar
            role_arn=SomeArn
            """.trimIndent()
        )

        createProviderFactory()

        writeProfileFile(
            """
            [profile foo]
            aws_access_key_id=FooAccessKey
            aws_secret_access_key=FooSecretKey
            aws_session_token=FooSessionToken

            [profile bar]
            source_profile=foo
            role_arn=DifferentArn

            [profile baz]
            source_profile=bar
            role_arn=SomeArn
            """.trimIndent()
        )

        mockProfileWatcher.triggerListeners()

        argumentCaptor<CredentialsChangeEvent> {
            verify(profileLoadCallback, times(2)).invoke(capture())

            assertThat(firstValue.added).hasSize(3).has(profileName("foo")).has(profileName("bar")).has(profileName("baz"))
            assertThat(firstValue.modified).isEmpty()
            assertThat(firstValue.removed).isEmpty()

            assertThat(secondValue.added).isEmpty()
            assertThat(secondValue.modified).hasSize(2).has(profileName("bar")).has(profileName("baz"))
            assertThat(secondValue.removed).isEmpty()
        }
    }

    @Test
    fun `removing a parent credential provider reports removal for it and its children`() {
        writeProfileFile(
            """
            [profile foo]
            aws_access_key_id=FooAccessKey
            aws_secret_access_key=FooSecretKey
            aws_session_token=FooSessionToken

            [profile bar]
            source_profile=foo
            role_arn=SomeArn

            [profile baz]
            source_profile=bar
            role_arn=SomeArn
            """.trimIndent()
        )

        createProviderFactory()

        writeProfileFile(
            """
            [profile foo]
            aws_access_key_id=FooAccessKey
            aws_secret_access_key=FooSecretKey
            aws_session_token=FooSessionToken

            [profile baz]
            source_profile=bar
            role_arn=SomeArn
            """.trimIndent()
        )

        mockProfileWatcher.triggerListeners()

        argumentCaptor<CredentialsChangeEvent> {
            verify(profileLoadCallback, times(2)).invoke(capture())

            assertThat(firstValue.added).hasSize(3).has(profileName("foo")).has(profileName("bar")).has(profileName("baz"))
            assertThat(firstValue.modified).isEmpty()
            assertThat(firstValue.removed).isEmpty()

            assertThat(secondValue.added).isEmpty()
            assertThat(secondValue.modified).isEmpty()
            assertThat(secondValue.removed).hasSize(2).has(profileName("bar")).has(profileName("baz"))
        }
    }

    @Test
    fun `only modified profiles are reported`() {
        writeProfileFile(
            """
            [profile foo]
            aws_access_key_id=FooAccessKey
            aws_secret_access_key=FooSecretKey

            [profile bar]
            aws_access_key_id=FooAccessKey
            aws_secret_access_key=FooSecretKey
            """.trimIndent()
        )

        createProviderFactory()

        writeProfileFile(
            """
            [profile foo]
            aws_access_key_id=FooAccessKey2
            aws_secret_access_key=FooSecretKey2

            [profile bar]
            aws_access_key_id=FooAccessKey
            aws_secret_access_key=FooSecretKey
            """.trimIndent()
        )

        mockProfileWatcher.triggerListeners()

        argumentCaptor<CredentialsChangeEvent> {
            verify(profileLoadCallback, times(2)).invoke(capture())

            assertThat(firstValue.added).hasSize(2).has(profileName("foo")).has(profileName("bar"))
            assertThat(firstValue.modified).isEmpty()
            assertThat(firstValue.removed).isEmpty()

            assertThat(secondValue.added).isEmpty()
            assertThat(secondValue.modified).hasSize(1).has(profileName("foo"))
            assertThat(secondValue.removed).isEmpty()
        }
    }

    @Test
    fun `a deleted profile throws error when trying to be retrieved`() {
        writeProfileFile(
            """
            [profile foo]
            aws_access_key_id=FooAccessKey
            aws_secret_access_key=FooSecretKey
            aws_session_token=FooSessionToken
            """.trimIndent()
        )

        val providerFactory = createProviderFactory()
        val validProfile = findCredentialIdentifier("foo")
        val credentialsProvider = providerFactory.createProvider(validProfile)

        assertThat(credentialsProvider.resolveCredentials()).isInstanceOfSatisfying(AwsSessionCredentials::class.java) {
            assertThat(it.accessKeyId()).isEqualTo("FooAccessKey")
            assertThat(it.secretAccessKey()).isEqualTo("FooSecretKey")
            assertThat(it.sessionToken()).isEqualTo("FooSessionToken")
        }

        FileUtil.delete(profileFile)

        mockProfileWatcher.triggerListeners()

        assertThatThrownBy {
            providerFactory.createProvider(validProfile)
        }.isInstanceOf(IllegalStateException::class.java)

        argumentCaptor<CredentialsChangeEvent> {
            verify(profileLoadCallback, times(2)).invoke(capture())

            assertThat(firstValue.added).hasSize(1).has(profileName("foo"))
            assertThat(firstValue.modified).isEmpty()
            assertThat(firstValue.removed).isEmpty()

            assertThat(secondValue.added).isEmpty()
            assertThat(secondValue.modified).isEmpty()
            assertThat(secondValue.removed).hasSize(1).has(profileName("foo"))
        }
    }

    @Test
    fun `static credentials profile creates a provider`() {
        writeProfileFile(
            """
            [profile static]
            aws_access_key_id=BarAccessKey
            aws_secret_access_key=BarSecretKey
            """.trimIndent()
        )

        val providerFactory = createProviderFactory()
        val validProfile = findCredentialIdentifier("static")
        val credentialsProvider = providerFactory.createProvider(validProfile).resolveCredentials()

        assertThat(credentialsProvider).isInstanceOfSatisfying<AwsBasicCredentials> {
            assertThat(it.accessKeyId()).isEqualTo("BarAccessKey")
            assertThat(it.secretAccessKey()).isEqualTo("BarSecretKey")
        }
    }

    @Test
    fun `static session credential profile creates a provider`() {
        writeProfileFile(
            """
            [profile staticSession]
            aws_access_key_id=FooAccessKey
            aws_secret_access_key=FooSecretKey
            aws_session_token=FooSessionToken
            """.trimIndent()
        )

        val providerFactory = createProviderFactory()
        val validProfile = findCredentialIdentifier("staticSession")
        val credentialsProvider = providerFactory.createProvider(validProfile).resolveCredentials()

        assertThat(credentialsProvider).isInstanceOfSatisfying<AwsSessionCredentials> {
            assertThat(it.accessKeyId()).isEqualTo("FooAccessKey")
            assertThat(it.secretAccessKey()).isEqualTo("FooSecretKey")
            assertThat(it.sessionToken()).isEqualTo("FooSessionToken")
        }
    }

    @Test
    fun `assume role profile with a source_profile creates a provider`() {
        writeProfileFile(
            """
            [profile role]
            role_arn=arn1
            role_session_name=testSession
            external_id=externalId
            source_profile=source_profile

            [profile source_profile]
            aws_access_key_id=BarAccessKey
            aws_secret_access_key=BarSecretKey
            """.trimIndent()
        )

        clientManager.create<StsClient>()

        val providerFactory = createProviderFactory()
        val validProfile = findCredentialIdentifier("role")
        val credentialsProvider = providerFactory.createProvider(validProfile)

        assertThat(credentialsProvider).isInstanceOfSatisfying<ProfileAssumeRoleProvider> {
            assertThat(it.parentProvider).isInstanceOf<StaticCredentialsProvider>()
        }
    }

    @Test
    fun `assume role profile with a credential_source of ec2 creates a provider`() {
        writeProfileFile(
            """
            [profile role]
            role_arn=arn1
            role_session_name=testSession
            credential_source=Ec2InstanceMetadata
            """.trimIndent()
        )

        clientManager.create<StsClient>()

        val providerFactory = createProviderFactory()
        val validProfile = findCredentialIdentifier("role")
        val credentialsProvider = providerFactory.createProvider(validProfile)

        assertThat(credentialsProvider).isInstanceOfSatisfying<ProfileAssumeRoleProvider> {
            assertThat(it.parentProvider).isInstanceOf<InstanceProfileCredentialsProvider>()
        }
    }

    @Test
    fun `assume role profile with a credential_source of ecs creates a provider`() {
        writeProfileFile(
            """
            [profile role]
            role_arn=arn1
            role_session_name=testSession
            credential_source=EcsContainer
            """.trimIndent()
        )

        clientManager.create<StsClient>()

        val providerFactory = createProviderFactory()
        val validProfile = findCredentialIdentifier("role")
        val credentialsProvider = providerFactory.createProvider(validProfile)

        assertThat(credentialsProvider).isInstanceOfSatisfying<ProfileAssumeRoleProvider> {
            assertThat(it.parentProvider).isInstanceOf<ContainerCredentialsProvider>()
        }
    }

    @Test
    fun `assume role profile with a credential_source of env vars creates a provider`() {
        writeProfileFile(
            """
            [profile role]
            role_arn=arn1
            role_session_name=testSession
            credential_source=Environment
            """.trimIndent()
        )

        clientManager.create<StsClient>()

        val providerFactory = createProviderFactory()
        val validProfile = findCredentialIdentifier("role")
        val credentialsProvider = providerFactory.createProvider(validProfile)

        assertThat(credentialsProvider).isInstanceOfSatisfying<ProfileAssumeRoleProvider> {
            assertThat(it.parentProvider).isInstanceOf<AwsCredentialsProviderChain>()
        }
    }

    @Test
    fun `sso profile creates a provider`() {
        writeProfileFile(
            """
            [profile sso]
            sso_start_url=ValidUrl
            sso_region=us-east-2
            sso_account_id=111222333444
            sso_role_name=RoleName
            """.trimIndent()
        )

        clientManager.create<SsoClient>()
        clientManager.create<SsoOidcClient>()

        val providerFactory = createProviderFactory()
        val validProfile = findCredentialIdentifier("sso")
        val credentialsProvider = providerFactory.createProvider(validProfile)

        assertThat(credentialsProvider).isInstanceOf<ProfileSsoProvider>()
    }

    @Test
    fun `credential process profile creates a provider`() {
        writeProfileFile(
            """
            [profile credProcess]
            credential_process=echo
            """.trimIndent()
        )

        val providerFactory = createProviderFactory()
        val validProfile = findCredentialIdentifier("credProcess")
        val credentialsProvider = providerFactory.createProvider(validProfile)

        assertThat(credentialsProvider).isInstanceOf<ToolkitCredentialProcessProvider>()
    }

    @Test
    fun `MFA profiles always require user action`() {
        writeProfileFile(
            """
            [profile role]
            role_arn=arn1
            role_session_name=testSession
            external_id=externalId
            mfa_serial=someSerialArn
            source_profile=source_profile

            [profile source_profile]
            aws_access_key_id=BarAccessKey
            aws_secret_access_key=BarSecretKey
            """.trimIndent()
        )

        createProviderFactory()

        assertThat((findCredentialIdentifier("role") as InteractiveCredential).userActionRequired()).isTrue
    }

    @Test
    fun `sso profiles only need user action if the token is invalid`() {
        writeProfileFile(
            """
            [profile valid]
            sso_start_url=ValidUrl
            sso_region=us-east-2
            sso_account_id=111222333444
            sso_role_name=RoleName

            [profile expired]
            sso_start_url=ExpiredUrl
            sso_region=us-east-2
            sso_account_id=111222333444
            sso_role_name=RoleName

            [profile validChain]
            source_profile = valid
            role_arn = AssumedRoleArn
            """.trimIndent()
        )

        val ssoCache = mock<SsoCache> {
            on { loadAccessToken(("ValidUrl")) }.thenReturn(mock())
            on { loadAccessToken(("ExpiredUrl")) }.thenReturn(null)
        }

        createProviderFactory(ssoCache)

        assertThat((findCredentialIdentifier("valid") as InteractiveCredential).userActionRequired()).isFalse
        assertThat((findCredentialIdentifier("expired") as InteractiveCredential).userActionRequired()).isTrue
        assertThat((findCredentialIdentifier("validChain") as InteractiveCredential).userActionRequired()).isFalse
    }

    private fun writeProfileFile(content: String) {
        FileUtil.createIfDoesntExist(profileFile)
        FileUtil.writeToFile(profileFile, content)
    }

    private fun profileName(expectedProfileName: String, defaultRegion: String? = null): Condition<Iterable<CredentialIdentifier>> =
        object : Condition<Iterable<CredentialIdentifier>>(expectedProfileName) {
            override fun matches(value: Iterable<CredentialIdentifier>): Boolean = value.any {
                it.id == "profile:$expectedProfileName" && defaultRegion?.let { dr -> it.defaultRegionId == dr } ?: true
            }
        }

    private fun createProviderFactory(ssoCache: SsoCache = mock()): ProfileCredentialProviderFactory {
        val factory = ProfileCredentialProviderFactory(ssoCache)
        factory.setUp(profileLoadCallback)

        return factory
    }

    private fun findCredentialIdentifier(profileName: String) = credentialChangeEvent.allValues.flatMap { it.added }.first { it.id == "profile:$profileName" }

    private fun ProfileCredentialProviderFactory.createProvider(validProfile: CredentialIdentifier) = this.createAwsCredentialProvider(
        validProfile,
        getDefaultRegion(),
    )

    private class MockProfileWatcher : ProfileWatcher {
        private val listeners = mutableListOf<() -> Unit>()

        override fun addListener(listener: () -> Unit) {
            listeners.add(listener)
        }

        fun reset() {
            listeners.clear()
        }

        fun triggerListeners() {
            listeners.forEach { it() }
        }
    }

    private companion object {
        const val MFA_TOKEN = "MfaToken"
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.credentials.profiles

import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import software.aws.toolkits.core.rules.SystemPropertyHelper
import software.aws.toolkits.resources.message
import java.io.File

class ProfileReaderTest {
    @Rule
    @JvmField
    val temporaryFolder = TemporaryFolder()

    @Rule
    @JvmField
    val systemPropertyHelper = SystemPropertyHelper()

    private lateinit var configFile: File
    private lateinit var credentialsFile: File

    @Before
    fun setUp() {
        configFile = temporaryFolder.newFile("config")
        credentialsFile = temporaryFolder.newFile("credentials")

        System.getProperties().setProperty("aws.configFile", configFile.absolutePath)
        System.getProperties().setProperty("aws.sharedCredentialsFile", credentialsFile.absolutePath)
    }

    @Test
    fun `source_profile points to a profile that does not exist`() {
        configFile.writeText(
            """
            [profile role]
            role_arn=arn1
            role_session_name=testSession
            source_profile=source_profile
            external_id=externalId
            """.trimIndent()
        )

        val (validProfiles, invalidProfiles) = validateAndGetProfiles()
        assertThat(validProfiles).isEmpty()
        assertThat(invalidProfiles.map { it.key to it.value.message })
            .contains("role" to message("credentials.profile.source_profile_not_found", "role", "source_profile"))
    }

    @Test
    fun `source_profile chain can't go in a circle`() {
        configFile.writeText(
            """
            [profile role]
            role_arn=arn1
            source_profile=source_profile

            [profile source_profile]
            role_arn=arn2
            source_profile=source_profile2

            [profile source_profile2]
            role_arn=arn3
            source_profile=source_profile3

            [profile source_profile3]
            role_arn=arn4
            source_profile=source_profile
            """.trimIndent()
        )

        val (validProfiles, invalidProfiles) = validateAndGetProfiles()
        assertThat(validProfiles).isEmpty()
        assertThat(invalidProfiles.map { it.key to it.value.message })
            .contains(
                "role" to message(
                    "credentials.profile.circular_profiles",
                    "role",
                    "role->source_profile->source_profile2->source_profile3->source_profile"
                )
            )
    }

    @Test
    fun `source_profile can't reference itself`() {
        configFile.writeText(
            """
            [profile role]
            role_arn=arn1
            source_profile=role
            """.trimIndent()
        )

        val (validProfiles, invalidProfiles) = validateAndGetProfiles()
        assertThat(validProfiles).isEmpty()
        assertThat(invalidProfiles.map { it.key to it.value.message })
            .contains("role" to message("credentials.profile.circular_profiles", "role", "role->role"))
    }

    @Test
    fun `assume a role requires either a source_profile or credential_source`() {
        configFile.writeText(
            """
            [profile role]
            role_arn = arn:aws:iam::xxx:role/<role>
            """.trimIndent()
        )

        val (validProfiles, invalidProfiles) = validateAndGetProfiles()
        assertThat(validProfiles).isEmpty()
        assertThat(invalidProfiles.map { it.key to it.value.message })
            .contains("role" to message("credentials.profile.assume_role.missing_source", "role"))
    }

    @Test
    fun `assume a role can't specify both a source_profile or credential_source`() {
        configFile.writeText(
            """
            [profile role]
            role_arn=arn1
            source_profile=source_profile
            credential_source=EcsContainer

            [profile source_profile]
            aws_access_key_id=BarAccessKey
            aws_secret_access_key=BarSecretKey
            """.trimIndent()
        )

        val (validProfiles, invalidProfiles) = validateAndGetProfiles()
        assertThat(validProfiles).containsKey("source_profile")
        assertThat(invalidProfiles.map { it.key to it.value.message })
            .contains("role" to message("credentials.profile.assume_role.duplicate_source", "role"))
    }

    @Test
    fun `credential_source with invalid type is invalid`() {
        configFile.writeText(
            """
            [profile role]
            role_arn=arn1
            credential_source=Foo
            """.trimIndent()
        )

        val (validProfiles, invalidProfiles) = validateAndGetProfiles()
        assertThat(validProfiles).isEmpty()
        assertThat(invalidProfiles.map { it.key to it.value.message })
            .contains("role" to message("credentials.profile.assume_role.invalid_credential_source", "role"))
    }

    @Test
    fun `assume a role with an invalid profile bubbles error`() {
        configFile.writeText(
            """
            [profile role]
            role_arn=arn1
            source_profile=source_profile

            [profile source_profile]
            role_arn=arn2
            """.trimIndent()
        )

        val (validProfiles, invalidProfiles) = validateAndGetProfiles()
        assertThat(validProfiles).isEmpty()
        assertThat(invalidProfiles.map { it.key to it.value.message })
            .contains("role" to message("credentials.profile.assume_role.missing_source", "source_profile"))
            .contains("source_profile" to message("credentials.profile.assume_role.missing_source", "source_profile"))
    }

    @Test
    fun `valid assume role with source_profile returns valid`() {
        configFile.writeText(
            """
            [profile role]
            role_arn=arn1
            source_profile=source_profile

            [profile source_profile]
            aws_access_key_id=BarAccessKey
            aws_secret_access_key=BarSecretKey
            """.trimIndent()
        )

        val (validProfiles, invalidProfiles) = validateAndGetProfiles()
        assertThat(validProfiles).hasSize(2)
        assertThat(invalidProfiles).isEmpty()
    }

    @Test
    fun `valid assume role with credential_source returns valid`() {
        configFile.writeText(
            """
            [profile role]
            role_arn=arn1
            credential_source=EcsContainer
            """.trimIndent()
        )

        val (validProfiles, invalidProfiles) = validateAndGetProfiles()
        assertThat(validProfiles).hasSize(1)
        assertThat(invalidProfiles).isEmpty()
    }

    @Test
    fun `valid assume role with credential_source in a chain returns valid`() {
        configFile.writeText(
            """
            [profile role]
            role_arn=arn1
            source_profile=source_profile

            [profile source_profile]
            role_arn=arn2
            source_profile=source_profile2

            [profile source_profile2]
            role_arn=arn3
            source_profile=source_profile3

            [profile source_profile3]
            role_arn=arn4
            credential_source=EcsContainer
            """.trimIndent()
        )

        val (validProfiles, invalidProfiles) = validateAndGetProfiles()
        assertThat(validProfiles).hasSize(4)
        assertThat(invalidProfiles).isEmpty()
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.credentials.profiles

import software.amazon.awssdk.profiles.Profile
import software.aws.toolkits.core.utils.test.aString

fun profile(name: String = aString(), properties: MutableMap<String, String>.() -> Unit = {}): Profile = Profile.builder()
    .name(name)
    .properties(mutableMapOf<String, String>().apply { properties(this) })
    .build()


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.credentials.profiles

import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Ref
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.impl.local.LocalFileSystemImpl
import com.intellij.openapi.vfs.newvfs.ManagingFS
import com.intellij.openapi.vfs.newvfs.RefreshQueue
import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.DisposableRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import software.aws.toolkits.core.rules.SystemPropertyHelper
import software.aws.toolkits.jetbrains.utils.spinUntil
import java.io.File
import java.time.Duration
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

class ProfileWatcherTest {
    companion object {
        @ClassRule
        @JvmField
        val disposableRule = DisposableRule()
    }

    @Rule
    @JvmField
    val application = ApplicationRule()

    @Rule
    @JvmField
    val temporaryFolder = TemporaryFolder()

    @Rule
    @JvmField
    val systemPropertyHelper = SystemPropertyHelper()

    private lateinit var awsFolder: File
    private lateinit var profileFile: File
    private lateinit var credentialsFile: File

    @Before
    fun setUp() {
        awsFolder = File(temporaryFolder.root, ".aws")
        profileFile = File(awsFolder, "config")
        credentialsFile = File(awsFolder, "credentials")

        System.getProperties().setProperty("aws.configFile", profileFile.absolutePath)
        System.getProperties().setProperty("aws.sharedCredentialsFile", credentialsFile.absolutePath)
    }

    @Test
    fun `watcher is notified on creation`() {
        profileFile.parentFile.mkdirs()

        assertFileChange {
            profileFile.writeText("Test")
        }
    }

    @Test
    fun `watcher is notified on edit`() {
        profileFile.parentFile.mkdirs()
        profileFile.writeText("Test")

        assertThat(LocalFileSystem.getInstance().refreshAndFindFileByIoFile(profileFile)).isNotNull

        assertFileChange {
            profileFile.writeText("Test2")
        }
    }

    @Test
    fun `watcher is notified on deletion`() {
        profileFile.parentFile.mkdirs()
        profileFile.writeText("Test")

        assertThat(LocalFileSystem.getInstance().refreshAndFindFileByIoFile(profileFile)).isNotNull

        assertFileChange {
            profileFile.delete()
        }

        assertThat(LocalFileSystem.getInstance().refreshAndFindFileByIoFile(profileFile)).isNull()
    }

    /**
     * These tests are complicated and reaching into some low level systems inside of the IDE to replicate how it works due stuff is disabled in unit test mode.
     *
     * First, FileWatcher (fsnotify[.exe]) that notifies the IDE that files are dirty is not ran in unit test mode. We start/stop it manually so that we can
     * validate external edits to the profile file is handled.
     *
     * Second, the system that marks VFS files dirty from the FileWatcher is only configured to run if FileWatcher is running in the constructor.
     * See com.intellij.openapi.vfs.impl.local.LocalFileSystemImpl constructor. Due to that, we schedule a manual VFS refresh to recheck all the files
     * marked dirty.
     */
    private fun assertFileChange(block: () -> Unit) {
        val fileWatcher = (LocalFileSystem.getInstance() as LocalFileSystemImpl).fileWatcher
        Disposer.register(
            disposableRule.disposable,
            Disposable {
                fileWatcher.shutdown()

                spinUntil(Duration.ofSeconds(10)) {
                    !fileWatcher.isOperational
                }
            }
        )

        val watcherTriggered = CountDownLatch(1)
        fileWatcher.startup {
            // Contains due to /private/ vs /
            if (it.contains(awsFolder.absolutePath)) {
                watcherTriggered.countDown()
            }
        }

        spinUntil(Duration.ofSeconds(10)) {
            fileWatcher.isOperational
        }

        val sut = DefaultProfileWatcher()
        Disposer.register(disposableRule.disposable, sut)

        spinUntil(Duration.ofSeconds(10)) {
            !fileWatcher.isSettingRoots
        }

        val updateCalled = Ref.create(false)
        sut.addListener { updateCalled.set(true) }

        block()

        // Wait for fsnotify to see the change
        assertThat(watcherTriggered.await(5, TimeUnit.SECONDS)).describedAs("FileWatcher is triggered").isTrue

        val refreshComplete = CountDownLatch(1)
        RefreshQueue.getInstance().refresh(true, true, { refreshComplete.countDown() }, *ManagingFS.getInstance().localRoots)

        // Wait for refresh to complete
        refreshComplete.await()

        // The update is triggered asynchronously, so this does not mean it's done, especially when the system is under high load
        // Since we are compiling ultimate/rider while running this test it can fail easily. 2000 is since we are dealing with the filesystem
        // so, pick a high arbitrary value
        if (updateCalled.get() == false) {
            Thread.sleep(2000)
        }
        assertThat(updateCalled.get()).describedAs("ProfileWatcher is triggered").isTrue
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.credentials.sso

import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.NioFiles
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.DisabledOnOs
import org.junit.jupiter.api.condition.OS
import org.junit.jupiter.api.io.TempDir
import software.aws.toolkits.core.utils.readText
import software.aws.toolkits.core.utils.writeText
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.attribute.PosixFilePermissions
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import kotlin.io.path.setPosixFilePermissions

class DiskCacheTest {
    private val now = Instant.now()
    private val clock = Clock.fixed(now, ZoneOffset.UTC)

    private val ssoUrl = "https://123456.awsapps.com/start"
    private val ssoRegion = "us-fake-1"
    private val scopes = listOf("scope1", "scope2")

    private lateinit var cacheRoot: Path
    private lateinit var cacheLocation: Path
    private lateinit var sut: DiskCache

    @BeforeEach
    fun setUp(@TempDir tempFolder: Path) {
        cacheRoot = tempFolder.toAbsolutePath()
        cacheLocation = Paths.get(cacheRoot.toString(), "fakehome", ".aws", "sso", "cache")
        Files.createDirectories(cacheLocation)
        sut = DiskCache(cacheLocation, clock)
    }

    @Test
    fun nonExistentClientRegistrationReturnsNull() {
        assertThat(sut.loadClientRegistration(ssoRegion)).isNull()
    }

    @Test
    fun corruptClientRegistrationReturnsNull() {
        cacheLocation.resolve("aws-toolkit-jetbrains-client-id-$ssoRegion.json").writeText("badData")

        assertThat(sut.loadClientRegistration(ssoRegion)).isNull()
    }

    @Test
    fun expiredClientRegistrationReturnsNull() {
        cacheLocation.resolve("aws-toolkit-jetbrains-client-id-$ssoRegion.json").writeText(
            """
            {
                "clientId": "DummyId",
                "clientSecret": "DummySecret",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(now.minusSeconds(100))}"
            }
            """.trimIndent()
        )

        assertThat(sut.loadClientRegistration(ssoRegion)).isNull()
    }

    @Test
    fun clientRegistrationExpiringSoonIsTreatedAsExpired() {
        val expirationTime = now.plus(14, ChronoUnit.MINUTES)
        cacheLocation.resolve("aws-toolkit-jetbrains-client-id-$ssoRegion.json").writeText(
            """
            {
                "clientId": "DummyId",
                "clientSecret": "DummySecret",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(expirationTime)}"
            }
            """.trimIndent()
        )

        assertThat(sut.loadClientRegistration(ssoRegion)).isNull()
    }

    @Test
    fun validClientRegistrationReturnsCorrectly() {
        val expirationTime = now.plus(20, ChronoUnit.MINUTES)
        cacheLocation.resolve("aws-toolkit-jetbrains-client-id-$ssoRegion.json").writeText(
            """
            {
                "clientId": "DummyId",
                "clientSecret": "DummySecret",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(expirationTime)}"
            }
            """.trimIndent()
        )

        assertThat(sut.loadClientRegistration(ssoRegion))
            .usingRecursiveComparison()
            .isEqualTo(
                ClientRegistration(
                    "DummyId",
                    "DummySecret",
                    expirationTime
                )
            )
    }

    @Test
    fun `valid scoped client registration loads correctly`() {
        val key = ClientRegistrationCacheKey(
            startUrl = ssoUrl,
            scopes = scopes,
            region = ssoRegion
        )
        val expirationTime = now.plus(20, ChronoUnit.MINUTES)
        cacheLocation.resolve("223224b6f0b4702c1a984be8284fe2c9d9718759.json").writeText(
            """
            {
                "clientId": "DummyId",
                "clientSecret": "DummySecret",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(expirationTime)}",
                "scopes": ["scope1","scope2"]
            }
            """.trimIndent()
        )

        assertThat(sut.loadClientRegistration(key))
            .usingRecursiveComparison()
            .isEqualTo(
                ClientRegistration(
                    "DummyId",
                    "DummySecret",
                    expirationTime,
                    scopes
                )
            )
    }

    @Test
    fun clientRegistrationSavesCorrectly() {
        val expirationTime = DateTimeFormatter.ISO_INSTANT.parse("2020-04-07T21:31:33Z")
        sut.saveClientRegistration(
            ssoRegion,
            ClientRegistration(
                "DummyId",
                "DummySecret",
                Instant.from(expirationTime)
            )
        )

        val clientRegistration = cacheLocation.resolve("aws-toolkit-jetbrains-client-id-$ssoRegion.json")
        if (SystemInfo.isUnix) {
            assertPosixPermissions(clientRegistration, "rw-------")
        }
        assertThat(clientRegistration.readText())
            .isEqualToIgnoringWhitespace(
                """
                {
                    "clientId": "DummyId",
                    "clientSecret": "DummySecret",
                    "expiresAt": "2020-04-07T21:31:33Z"
                }
                """.trimIndent()
            )
    }

    @Test
    fun `scoped client registration saves correctly`() {
        val key = ClientRegistrationCacheKey(
            startUrl = ssoUrl,
            scopes = scopes,
            region = ssoRegion
        )
        val expirationTime = DateTimeFormatter.ISO_INSTANT.parse("2020-04-07T21:31:33Z")
        sut.saveClientRegistration(
            key,
            ClientRegistration(
                "DummyId",
                "DummySecret",
                Instant.from(expirationTime),
                scopes
            )
        )

        val clientRegistration = cacheLocation.resolve("223224b6f0b4702c1a984be8284fe2c9d9718759.json")
        if (SystemInfo.isUnix) {
            assertPosixPermissions(clientRegistration, "rw-------")
        }
        assertThat(clientRegistration.readText())
            .isEqualToIgnoringWhitespace(
                """
                {
                    "clientId": "DummyId",
                    "clientSecret": "DummySecret",
                    "expiresAt": "2020-04-07T21:31:33Z",
                    "scopes": ["scope1","scope2"]
                }
                """.trimIndent()
            )
    }

    @Test
    fun invalidateClientRegistrationDeletesTheFile() {
        val expirationTime = now.plus(20, ChronoUnit.MINUTES)
        val cacheFile = cacheLocation.resolve("aws-toolkit-jetbrains-client-id-$ssoRegion.json")
        cacheFile.writeText(
            """
            {
                "clientId": "DummyId",
                "clientSecret": "DummySecret",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(expirationTime)}"
            }
            """.trimIndent()
        )

        assertThat(sut.loadClientRegistration(ssoRegion)).isNotNull()

        sut.invalidateClientRegistration(ssoRegion)

        assertThat(sut.loadClientRegistration(ssoRegion)).isNull()
        assertThat(cacheFile).doesNotExist()
    }

    @Test
    fun `invalidate scoped client registration deletes the file`() {
        val expirationTime = now.plus(20, ChronoUnit.MINUTES)
        val cacheFile = cacheLocation.resolve("223224b6f0b4702c1a984be8284fe2c9d9718759.json")
        cacheFile.writeText(
            """
            {
                "clientId": "DummyId",
                "clientSecret": "DummySecret",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(expirationTime)}"
            }
            """.trimIndent()
        )

        val key = ClientRegistrationCacheKey(
            startUrl = ssoUrl,
            scopes = scopes,
            region = ssoRegion
        )

        assertThat(sut.loadClientRegistration(key)).isNotNull()

        sut.invalidateClientRegistration(key)

        assertThat(sut.loadClientRegistration(key)).isNull()
        assertThat(cacheFile).doesNotExist()
    }

    @Test
    fun nonExistentAccessTokenReturnsNull() {
        assertThat(sut.loadAccessToken(ssoUrl)).isNull()
    }

    @Test
    fun corruptAccessTokenReturnsNull() {
        cacheLocation.resolve("c1ac99f782ad92755c6de8647b510ec247330ad1.json").writeText("badData")

        assertThat(sut.loadAccessToken(ssoUrl)).isNull()
    }

    @Test
    fun `expired access token is not loaded`() {
        cacheLocation.resolve("c1ac99f782ad92755c6de8647b510ec247330ad1.json").writeText(
            """
            {
                "startUrl": "$ssoUrl",
                "region": "$ssoRegion",
                "accessToken": "DummyAccessToken",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(now.minusSeconds(100))}",
            }
            """.trimIndent()
        )

        assertThat(sut.loadAccessToken(ssoUrl)).isNull()
    }

    @Test
    fun `expired access token is loaded if it has a refresh token`() {
        cacheLocation.resolve("c1ac99f782ad92755c6de8647b510ec247330ad1.json").writeText(
            """
            {
                "startUrl": "$ssoUrl",
                "region": "$ssoRegion",
                "accessToken": "DummyAccessToken",
                "refreshToken": "ARefreshToken",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(now.minusSeconds(100))}"
            }
            """.trimIndent()
        )

        assertThat(sut.loadAccessToken(ssoUrl)).isNotNull()
    }

    @Test
    fun accessTokenExpiringSoonIsTreatedAsExpired() {
        val expirationTime = now.plus(14, ChronoUnit.MINUTES)
        cacheLocation.resolve("c1ac99f782ad92755c6de8647b510ec247330ad1.json").writeText(
            """
            {
                "startUrl": "$ssoUrl",
                "region": "$ssoRegion",
                "accessToken": "DummyAccessToken",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(expirationTime)}"
            }
            """.trimIndent()
        )

        assertThat(sut.loadAccessToken(ssoUrl)).isNull()
    }

    @Test
    fun validAccessTokenReturnsCorrectly() {
        val expirationTime = now.plus(20, ChronoUnit.MINUTES)
        cacheLocation.resolve("c1ac99f782ad92755c6de8647b510ec247330ad1.json").writeText(
            """
            {
                "startUrl": "$ssoUrl",
                "region": "$ssoRegion",
                "accessToken": "DummyAccessToken",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(expirationTime)}"
            }
            """.trimIndent()
        )

        assertThat(sut.loadAccessToken(ssoUrl))
            .usingRecursiveComparison()
            .isEqualTo(
                AccessToken(
                    ssoUrl,
                    ssoRegion,
                    "DummyAccessToken",
                    expiresAt = expirationTime
                )
            )
    }

    @Test
    fun validAccessTokenFromCliReturnsCorrectly() {
        cacheLocation.resolve("c1ac99f782ad92755c6de8647b510ec247330ad1.json").writeText(
            """
            {
                "startUrl": "$ssoUrl",
                "region": "$ssoRegion",
                "accessToken": "DummyAccessToken",
                "expiresAt": "2999-06-10T00:50:40UTC"
            }
            """.trimIndent()
        )

        assertThat(sut.loadAccessToken(ssoUrl))
            .usingRecursiveComparison()
            .isEqualTo(
                AccessToken(
                    ssoUrl,
                    ssoRegion,
                    "DummyAccessToken",
                    expiresAt = ZonedDateTime.of(2999, 6, 10, 0, 50, 40, 0, ZoneOffset.UTC).toInstant()
                )
            )
    }

    @Test
    fun accessTokenSavesCorrectly() {
        val expirationTime = DateTimeFormatter.ISO_INSTANT.parse("2020-04-07T21:31:33Z")
        sut.saveAccessToken(
            ssoUrl,
            AccessToken(
                ssoUrl,
                ssoRegion,
                "DummyAccessToken",
                expiresAt = Instant.from(expirationTime),
                createdAt = Instant.EPOCH
            )
        )

        val accessTokenCache = cacheLocation.resolve("c1ac99f782ad92755c6de8647b510ec247330ad1.json")
        if (SystemInfo.isUnix) {
            assertPosixPermissions(accessTokenCache, "rw-------")
        }

        assertThat(accessTokenCache.readText())
            .isEqualToIgnoringWhitespace(
                """
                {
                    "startUrl": "$ssoUrl",
                    "region": "$ssoRegion",
                    "accessToken": "DummyAccessToken",
                    "expiresAt": "2020-04-07T21:31:33Z",
                    "createdAt":"1970-01-01T00:00:00Z"
                }
                """.trimIndent()
            )
    }

    @Test
    fun `scoped access token saves correctly`() {
        val key = AccessTokenCacheKey("connectionId", ssoUrl, listOf("scope1", "scope2"))
        val expirationTime = DateTimeFormatter.ISO_INSTANT.parse("2020-04-07T21:31:33Z")
        sut.saveAccessToken(
            key,
            AccessToken(
                ssoUrl,
                ssoRegion,
                "DummyAccessToken",
                "RefreshToken",
                Instant.from(expirationTime)
            )
        )

        val accessTokenCache = cacheLocation.resolve("72286fb950f12c77c840239851fd64ac60275c5c.json")
        if (SystemInfo.isUnix) {
            assertPosixPermissions(accessTokenCache, "rw-------")
        }

        assertThat(accessTokenCache.readText())
            .isEqualToIgnoringWhitespace(
                """
                {
                    "startUrl": "$ssoUrl",
                    "region": "$ssoRegion",
                    "accessToken": "DummyAccessToken",
                    "refreshToken": "RefreshToken",
                    "expiresAt": "2020-04-07T21:31:33Z",
                    "createdAt":"1970-01-01T00:00:00Z"
                }
                """.trimIndent()
            )
    }

    @Test
    fun accessTokenInvalidationDeletesFile() {
        val expirationTime = now.plus(20, ChronoUnit.MINUTES)
        val cacheFile = cacheLocation.resolve("c1ac99f782ad92755c6de8647b510ec247330ad1.json")
        cacheFile.writeText(
            """
            {
                "startUrl": "$ssoUrl",
                "region": "$ssoRegion",
                "accessToken": "DummyAccessToken",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(expirationTime)}",
                "createdAt":"1970-01-01T00:00:00Z"
            }
            """.trimIndent()
        )

        assertThat(sut.loadAccessToken(ssoUrl)).isNotNull()

        sut.invalidateAccessToken(ssoUrl)

        assertThat(sut.loadAccessToken(ssoUrl)).isNull()
        assertThat(cacheFile).doesNotExist()
    }

    @Test
    fun `invalidate scoped access token deletes file`() {
        val expirationTime = now.plus(20, ChronoUnit.MINUTES)
        val cacheFile = cacheLocation.resolve("72286fb950f12c77c840239851fd64ac60275c5c.json")
        val key = AccessTokenCacheKey("connectionId", ssoUrl, listOf("scope1", "scope2"))

        cacheFile.writeText(
            """
            {
                "startUrl": "$ssoUrl",
                "region": "$ssoRegion",
                "accessToken": "DummyAccessToken",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(expirationTime)}",
                "createdAt":"1970-01-01T00:00:00Z"
            }
            """.trimIndent()
        )

        assertThat(sut.loadAccessToken(key)).isNotNull()

        sut.invalidateAccessToken(key)

        assertThat(sut.loadAccessToken(key)).isNull()
        assertThat(cacheFile).doesNotExist()
    }

    @Test
    fun `scope order does not matter for scoped access token cache`() {
        val expirationTime = now.plus(20, ChronoUnit.MINUTES)
        val cacheFile = cacheLocation.resolve("72286fb950f12c77c840239851fd64ac60275c5c.json")
        val key1 = AccessTokenCacheKey("connectionId", ssoUrl, listOf("scope1", "scope2"))
        val key2 = AccessTokenCacheKey("connectionId", ssoUrl, listOf("scope2", "scope1"))

        cacheFile.writeText(
            """
            {
                "startUrl": "$ssoUrl",
                "region": "$ssoRegion",
                "accessToken": "DummyAccessToken",
                "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(expirationTime)}",
                "createdAt":"1970-01-01T00:00:00Z"
            }
            """.trimIndent()
        )

        assertThat(sut.loadAccessToken(key1)).isNotNull()
        assertThat(sut.loadAccessToken(key2)).isNotNull()
    }

    @Test
    @DisabledOnOs(OS.WINDOWS)
    fun `handles error saving client registration when user home is not writable`() {
        Files.newDirectoryStream(cacheRoot).forEach { NioFiles.deleteRecursively(it) }
        cacheRoot.resolve("fakehome").apply {
            Files.createDirectory(this)
            setPosixFilePermissions(emptySet())
        }
        cacheRoot.setPosixFilePermissions(PosixFilePermissions.fromString("r-xr-xr-x"))

        sut.saveClientRegistration(
            ssoRegion,
            ClientRegistration(
                "DummyId",
                "DummySecret",
                Instant.now()
            )
        )

        val registration = cacheLocation.resolve("aws-toolkit-jetbrains-client-id-$ssoRegion.json")
        assertThat(cacheLocation).isEqualTo(Paths.get(cacheRoot.toString(), "fakehome", ".aws", "sso", "cache"))
        assertPosixPermissions(cacheRoot, "rwxr-xr-x")
        assertPosixPermissions(cacheRoot.resolve("fakehome"), "rwx------")
        assertPosixPermissions(cacheRoot.resolve("fakehome").resolve(".aws"), "rwxr-xr-x")
        assertPosixPermissions(cacheRoot.resolve("fakehome").resolve(".aws").resolve("sso"), "rwxr-xr-x")
        assertPosixPermissions(cacheRoot.resolve("fakehome").resolve(".aws").resolve("sso").resolve("cache"), "rwxr-xr-x")
        assertPosixPermissions(registration, "rw-------")
    }

    @Test
    @DisabledOnOs(OS.WINDOWS)
    fun `handles error saving client registration when client registration is not writable`() {
        val registration = cacheLocation.resolve("aws-toolkit-jetbrains-client-id-$ssoRegion.json")
        sut.saveClientRegistration(
            ssoRegion,
            ClientRegistration(
                "DummyId",
                "DummySecret",
                Instant.now()
            )
        )

        registration.setPosixFilePermissions(emptySet())
        assertPosixPermissions(registration, "---------")

        sut.saveClientRegistration(
            ssoRegion,
            ClientRegistration(
                "DummyId",
                "DummySecret",
                Instant.now()
            )
        )

        assertPosixPermissions(registration, "rw-------")
    }

    @Test
    @DisabledOnOs(OS.WINDOWS)
    fun `handles reading client registration when file is owned but not readable`() {
        sut.saveClientRegistration(
            ssoRegion,
            ClientRegistration(
                "DummyId",
                "DummySecret",
                Instant.MAX
            )
        )
        val registration = cacheLocation.resolve("aws-toolkit-jetbrains-client-id-$ssoRegion.json")
        registration.setPosixFilePermissions(emptySet())
        assertPosixPermissions(registration, "---------")

        assertThat(sut.loadClientRegistration(ssoRegion)).isNotNull()

        assertPosixPermissions(registration, "rw-------")
    }

    private fun assertPosixPermissions(path: Path, expected: String) {
        val perms = PosixFilePermissions.toString(Files.getPosixFilePermissions(path))
        assertThat(perms).isEqualTo(expected)
    }
}


// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.core.credentials.sso

import com.intellij.openapi.components.service
import org.junit.jupiter.api.Assumptions.assumeTrue
import software.amazon.awssdk.core.SdkBytes
import software.amazon.awssdk.services.lambda.LambdaClient
import software.amazon.awssdk.services.lambda.model.InvocationType
import software.amazon.awssdk.services.lambda.model.LambdaException
import software.aws.toolkits.core.clients.nullDefaultProfileFile
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.utils.scrubException

internal class MockSsoLoginCallbackProvider : SsoLoginCallbackProvider {
    internal var provider: SsoLoginCallback? = null
    private object ErrorSsoLoginCallback : SsoLoginCallback {
        override fun tokenPending(authorization: Authorization) {
            error("Not implemented")
        }

        override fun tokenRetrieved() {}

        override fun tokenRetrievalFailure(e: Exception) {}
    }

    override fun getProvider(ssoUrl: String): SsoLoginCallback =
        provider ?: ErrorSsoLoginCallback

    companion object {
        fun getInstance() = service<SsoLoginCallbackProvider>() as MockSsoLoginCallbackProvider
    }
}

object NoOpSsoLoginCallback : SsoLoginCallback {
    override fun tokenPending(authorization: Authorization) {}

    override fun tokenRetrieved() {}

    override fun tokenRetrievalFailure(e: Exception) {}
}

internal class TestSsoPrompt(private val secretName: String) : SsoLoginCallback {
    override fun tokenPending(authorization: Authorization) {
        val authLambda = System.getenv(authLambdaArn)
        assumeTrue(authLambda != null) {
            "Skipping test since $authLambdaArn wasn't set".also {
                LOG.warn { it }
            }
        }

        LOG.info { "Invoking login with authorization: $authorization" }

        // language=JSON
        val payload = """{
            |  "secret": "$secretName",
            |  "userCode": "${authorization.userCode}",
            |  "verificationUri": "${authorization.verificationUri}"
            |}
        """.trimMargin()

        val response = try {
            LambdaClient.builder()
                .overrideConfiguration {
                    if (!System.getenv("CI").isNullOrBlank()) {
                        it.nullDefaultProfileFile()
                    }
                }
                .build().use { client ->
                    client.invoke {
                        it.functionName(authLambda)
                        it.payload(SdkBytes.fromUtf8String(payload))
                        it.invocationType(InvocationType.REQUEST_RESPONSE)
                    }
                }
        } catch (e: LambdaException) {
            throw scrubException(e)
        }

        LOG.info {
            """
                "Auth invocation request ID: ${response.responseMetadata().requestId()}"
                "Auth function error: ${response.functionError()}
                "Auth invocation response status code: ${response.statusCode()}"
            """.trimIndent()
        }
    }

    override fun tokenRetrieved() {
        LOG.info { "Token successfully retrieved" }
    }

    override fun tokenRetrievalFailure(e: Exception) {
        LOG.error(e) { "Failed to retrieve token" }
    }

    private companion object {
        const val authLambdaArn = "AUTH_UTIL_LAMBDA_ARN"

        private val LOG = getLogger<TestSsoPrompt>()
    }
}