// 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.testFramework.ApplicationRule
import com.intellij.testFramework.RuleChain
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.KStubbing
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
import software.amazon.awssdk.services.ssooidc.model.AuthorizationPendingException
import software.amazon.awssdk.services.ssooidc.model.CreateTokenRequest
import software.amazon.awssdk.services.ssooidc.model.CreateTokenResponse
import software.amazon.awssdk.services.ssooidc.model.InvalidClientException
import software.amazon.awssdk.services.ssooidc.model.InvalidRequestException
import software.amazon.awssdk.services.ssooidc.model.RegisterClientRequest
import software.amazon.awssdk.services.ssooidc.model.RegisterClientResponse
import software.amazon.awssdk.services.ssooidc.model.SlowDownException
import software.amazon.awssdk.services.ssooidc.model.SsoOidcException
import software.amazon.awssdk.services.ssooidc.model.StartDeviceAuthorizationRequest
import software.amazon.awssdk.services.ssooidc.model.StartDeviceAuthorizationResponse
import software.aws.toolkits.core.region.aRegionId
import software.aws.toolkits.core.utils.delegateMock
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.jetbrains.utils.rules.SsoLoginCallbackProviderRule
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.time.ZoneOffset
import java.time.temporal.ChronoUnit

class SsoAccessTokenProviderTest {
    private val clock = Clock.fixed(Instant.now().truncatedTo(ChronoUnit.MILLIS), ZoneOffset.UTC)

    private val ssoUrl = aString()
    private val ssoRegion = aRegionId()
    private val clientId = aString()
    private val clientSecret = aString()

    private lateinit var ssoOidcClient: SsoOidcClient
    private lateinit var sut: SsoAccessTokenProvider
    private lateinit var ssoCache: SsoCache

    private val applicationRule = ApplicationRule()
    private val ssoCallbackRule = SsoLoginCallbackProviderRule()

    @JvmField
    @Rule
    val ruleChain = RuleChain(applicationRule, ssoCallbackRule)

    @Before
    fun setUp() {
        ssoOidcClient = delegateMock()
        ssoCache = mock()

        sut = SsoAccessTokenProvider(ssoUrl, ssoRegion, ssoCache, ssoOidcClient, clock = clock)
    }

    @Test
    fun getAccessTokenWithAccessTokenCache() {
        val accessToken = AccessToken(ssoUrl, ssoRegion, "dummyToken", expiresAt = clock.instant())
        ssoCache.stub {
            on(
                ssoCache.loadAccessToken(ssoUrl)
            ).thenReturn(
                accessToken
            )
        }

        val accessTokenActual = runBlocking { sut.accessToken() }
        assertThat(accessTokenActual)
            .usingRecursiveComparison()
            .isEqualTo(accessToken)

        verify(ssoCache).loadAccessToken(ssoUrl)
    }

    @Test
    fun getAccessTokenWithClientRegistrationCache() {
        val expirationClientRegistration = clock.instant().plusSeconds(120)
        setupCacheStub(expirationClientRegistration)

        ssoOidcClient.stub {
            stubStartDeviceAuthorization()
            stubCreateToken()
        }

        val accessToken = runBlocking { sut.accessToken() }
        assertThat(accessToken).usingRecursiveComparison()
            .isEqualTo(
                AccessToken(
                    ssoUrl,
                    ssoRegion,
                    "accessToken",
                    expiresAt = clock.instant().plusSeconds(180),
                    createdAt = sut.authorizationCreationTime

                )
            )

        verify(ssoOidcClient).startDeviceAuthorization(any<StartDeviceAuthorizationRequest>())
        verify(ssoOidcClient).createToken(any<CreateTokenRequest>())
        verify(ssoCache).loadAccessToken(ssoUrl)
        verify(ssoCache).loadClientRegistration(ssoRegion)
        verify(ssoCache).saveAccessToken(ssoUrl, accessToken)
    }

    @Test
    fun getAccessTokenWithoutCaches() {
        val expirationClientRegistration = clock.instant().plusSeconds(120)
        setupCacheStub(returnValue = null)

        ssoOidcClient.stub {
            on(
                ssoOidcClient.registerClient(
                    RegisterClientRequest.builder()
                        .clientType("public")
                        .clientName("AWS Toolkit for JetBrains")
                        .scopes(emptyList())
                        .build()
                )
            ).thenReturn(
                RegisterClientResponse.builder()
                    .clientId(clientId)
                    .clientSecret(clientSecret)
                    .clientSecretExpiresAt(expirationClientRegistration.toEpochMilli())
                    .build()
            )

            stubStartDeviceAuthorization()
            stubCreateToken()
        }

        val accessToken = runBlocking { sut.accessToken() }
        assertThat(accessToken).usingRecursiveComparison()
            .isEqualTo(
                AccessToken(
                    ssoUrl,
                    ssoRegion,
                    "accessToken",
                    expiresAt = clock.instant().plusSeconds(180),
                    createdAt = sut.authorizationCreationTime
                )
            )

        verify(ssoOidcClient).registerClient(any<RegisterClientRequest>())
        verify(ssoOidcClient).startDeviceAuthorization(any<StartDeviceAuthorizationRequest>())
        verify(ssoOidcClient).createToken(any<CreateTokenRequest>())
        verify(ssoCache).loadAccessToken(ssoUrl)
        verify(ssoCache).loadClientRegistration(ssoRegion)
        verify(ssoCache).saveClientRegistration(eq(ssoRegion), any())
        verify(ssoCache).saveAccessToken(ssoUrl, accessToken)
    }

    @Test
    fun getAccessTokenWithoutCachesMultiplePolls() {
        val expirationClientRegistration = clock.instant().plusSeconds(120)

        setupCacheStub(expirationClientRegistration)

        ssoOidcClient.stub {
            stubStartDeviceAuthorization(interval = 1)
            on(
                ssoOidcClient.createToken(createTokenRequest())
            ).thenThrow(
                AuthorizationPendingException.builder().build()
            ).thenReturn(
                createTokenResponse()
            )
        }

        val startTime = Instant.now()
        val accessToken = runBlocking { sut.accessToken() }
        val callDuration = Duration.between(startTime, Instant.now())
        val creationTime = sut.authorizationCreationTime

        assertThat(accessToken).usingRecursiveComparison()
            .isEqualTo(
                AccessToken(
                    ssoUrl,
                    ssoRegion,
                    "accessToken",
                    expiresAt = clock.instant().plusSeconds(180),
                    createdAt = creationTime
                )
            )

        assertThat(callDuration.seconds).isGreaterThanOrEqualTo(1).isLessThan(2)

        verify(ssoOidcClient).startDeviceAuthorization(any<StartDeviceAuthorizationRequest>())
        verify(ssoOidcClient, times(2)).createToken(any<CreateTokenRequest>())
        verify(ssoCache).loadAccessToken(ssoUrl)
        verify(ssoCache).loadClientRegistration(ssoRegion)
        verify(ssoCache).saveAccessToken(ssoUrl, accessToken)
    }

    @Test
    fun `refresh access token updates caches`() {
        val expirationClientRegistration = clock.instant().plusSeconds(120)
        setupCacheStub(expirationClientRegistration)

        val accessToken = AccessToken(ssoUrl, ssoRegion, "dummyToken", "refreshToken", clock.instant())
        ssoCache.stub {
            on(
                ssoCache.loadAccessToken(ssoUrl)
            ).thenReturn(
                accessToken
            )
        }

        ssoOidcClient.stub {
            on(
                ssoOidcClient.createToken(refreshTokenRequest())
            ).thenReturn(
                refreshTokenResponse()
            )
        }

        val refreshedToken = runBlocking { sut.refreshToken(sut.accessToken()) }

        verify(ssoCache).loadAccessToken(ssoUrl)
        verify(ssoCache).loadClientRegistration(ssoRegion)
        verify(ssoOidcClient).createToken(any<CreateTokenRequest>())
        verify(ssoCache).saveAccessToken(ssoUrl, refreshedToken)
    }

    @Test
    fun exceptionStopsPolling() {
        val expirationClientRegistration = clock.instant().plusSeconds(120)

        setupCacheStub(expirationClientRegistration)

        ssoOidcClient.stub {
            stubStartDeviceAuthorization()
            stubCreateToken(throws = true)
        }

        assertThatThrownBy { runBlocking { sut.accessToken() } }.isInstanceOf(InvalidRequestException::class.java)

        verify(ssoOidcClient).startDeviceAuthorization(any<StartDeviceAuthorizationRequest>())
        verify(ssoOidcClient).createToken(any<CreateTokenRequest>())
        verify(ssoCache).loadAccessToken(ssoUrl)
        verify(ssoCache).loadClientRegistration(ssoRegion)
    }

    @Test
    fun backOffTimeIsRespected() {
        val expirationClientRegistration = clock.instant().plusSeconds(120)
        setupCacheStub(expirationClientRegistration)

        ssoOidcClient.stub {
            stubStartDeviceAuthorization(interval = 1)

            on(
                ssoOidcClient.createToken(createTokenRequest())
            ).thenThrow(
                SlowDownException.builder().build()
            ).thenReturn(
                createTokenResponse()
            )
        }

        val startTime = Instant.now()
        val accessToken = runBlocking { sut.accessToken() }
        val callDuration = Duration.between(startTime, Instant.now())

        assertThat(accessToken).usingRecursiveComparison()
            .isEqualTo(
                AccessToken(
                    ssoUrl,
                    ssoRegion,
                    "accessToken",
                    expiresAt = clock.instant().plusSeconds(180),
                    createdAt = sut.authorizationCreationTime
                )
            )

        assertThat(callDuration).isGreaterThan(Duration.ofSeconds(5))

        verify(ssoCache).saveAccessToken(ssoUrl, accessToken)

        verify(ssoOidcClient).startDeviceAuthorization(any<StartDeviceAuthorizationRequest>())
        verify(ssoOidcClient, times(2)).createToken(any<CreateTokenRequest>())
        verify(ssoCache).loadAccessToken(ssoUrl)
        verify(ssoCache).loadClientRegistration(ssoRegion)
        verify(ssoCache).saveAccessToken(ssoUrl, accessToken)
    }

    @Test
    fun failToGetClientRegistrationLeadsToError() {
        setupCacheStub(returnValue = null)

        ssoOidcClient.stub {
            on(
                ssoOidcClient.registerClient(any<RegisterClientRequest>())
            ).thenThrow(
                SsoOidcException.builder().build()
            )
        }

        assertThatThrownBy { runBlocking { sut.accessToken() } }.isInstanceOf(SsoOidcException::class.java)

        verify(ssoOidcClient).registerClient(any<RegisterClientRequest>())
        verify(ssoCache).loadAccessToken(ssoUrl)
        verify(ssoCache).loadClientRegistration(ssoRegion)
    }

    @Test
    fun invalidClientRegistrationClearsTheCache() {
        setupCacheStub(Instant.now(clock))

        ssoOidcClient.stub {
            on(
                ssoOidcClient.startDeviceAuthorization(any<StartDeviceAuthorizationRequest>())
            ).thenThrow(
                InvalidClientException.builder().build()
            )
        }

        assertThatThrownBy { runBlocking { sut.accessToken() } }.isInstanceOf(InvalidClientException::class.java)

        verify(ssoCache).invalidateClientRegistration(ssoRegion)
    }

    @Test
    fun invalidateClearsTheCache() {
        sut.invalidate()

        verify(ssoCache).invalidateAccessToken(ssoUrl)
    }

    private fun setupCacheStub(expirationClientRegistration: Instant) {
        setupCacheStub(ClientRegistration(clientId, clientSecret, expirationClientRegistration))
    }

    private fun setupCacheStub(returnValue: ClientRegistration?) {
        ssoCache.stub {
            on(
                ssoCache.loadAccessToken(ssoUrl)
            ).thenReturn(
                null
            )

            on(
                ssoCache.loadClientRegistration(ssoRegion)
            ).thenReturn(
                returnValue
            )
        }
    }

    private fun KStubbing<SsoOidcClient>.stubStartDeviceAuthorization(interval: Int? = null) {
        on(
            ssoOidcClient.startDeviceAuthorization(
                StartDeviceAuthorizationRequest.builder()
                    .clientId(clientId)
                    .clientSecret(clientSecret)
                    .startUrl(ssoUrl)
                    .build()
            )
        ).thenReturn(
            StartDeviceAuthorizationResponse.builder()
                .expiresIn(120)
                .deviceCode("dummyCode")
                .userCode("dummyUserCode")
                .verificationUri("someUrl")
                .verificationUriComplete("someUrlComplete")
                .apply { if (interval != null) interval(interval) }
                .build()
        )
    }

    private fun KStubbing<SsoOidcClient>.stubCreateToken(throws: Boolean = false) {
        on(
            ssoOidcClient.createToken(createTokenRequest())
        ).apply {
            if (throws) {
                thenThrow(InvalidRequestException.builder().build())
            } else {
                thenReturn(createTokenResponse())
            }
        }
    }

    private fun createTokenRequest(): CreateTokenRequest = CreateTokenRequest.builder()
        .clientId(clientId)
        .clientSecret(clientSecret)
        .deviceCode("dummyCode")
        .grantType("urn:ietf:params:oauth:grant-type:device_code")
        .build()

    private fun createTokenResponse(): CreateTokenResponse = CreateTokenResponse.builder()
        .accessToken("accessToken")
        .expiresIn(180)
        .build()

    private fun refreshTokenRequest(): CreateTokenRequest = CreateTokenRequest.builder()
        .clientId(clientId)
        .clientSecret(clientSecret)
        .refreshToken("refreshToken")
        .grantType("refresh_token")
        .build()

    private fun refreshTokenResponse(): CreateTokenResponse = CreateTokenResponse.builder()
        .accessToken("accessToken2")
        .refreshToken("refreshToken2")
        .expiresIn(180)
        .build()
}


// 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 org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
import software.amazon.awssdk.services.sso.SsoClient
import software.amazon.awssdk.services.sso.model.GetRoleCredentialsRequest
import software.amazon.awssdk.services.sso.model.GetRoleCredentialsResponse
import software.amazon.awssdk.services.sso.model.RoleCredentials
import software.amazon.awssdk.services.sso.model.UnauthorizedException
import software.aws.toolkits.core.utils.delegateMock
import software.aws.toolkits.core.utils.test.aString
import java.time.Instant
import java.time.temporal.ChronoUnit

class SsoCredentialProviderTest {
    private val accessTokenId = aString()
    private val accountId = aString()
    private val roleName = aString()
    private val accessKey = aString()
    private val secretKey = aString()
    private val sessionToken = aString()
    private val credentials = AwsSessionCredentials.create(accessKey, secretKey, sessionToken)
    private val accessToken = AccessToken(aString(), aString(), accessTokenId, expiresAt = Instant.now().plusSeconds(10))

    private lateinit var ssoClient: SsoClient
    private lateinit var ssoAccessTokenProvider: SsoAccessTokenProvider
    private lateinit var sut: SsoCredentialProvider

    @Before
    fun setUp() {
        ssoClient = delegateMock()
        ssoAccessTokenProvider = mock {
            onBlocking {
                it.accessToken()
            }.thenReturn(
                accessToken
            )
        }
        sut = SsoCredentialProvider(accountId, roleName, ssoClient, ssoAccessTokenProvider)
    }

    @Test
    fun cachingDoesNotApplyToExpiredSession() {
        createSsoResponse(Instant.now().minusSeconds(5000))

        assertThat(sut.resolveCredentials()).usingRecursiveComparison().isEqualTo(credentials)

        // Resolve again
        assertThat(sut.resolveCredentials()).usingRecursiveComparison().isEqualTo(credentials)

        verify(ssoClient, times(2)).getRoleCredentials(any<GetRoleCredentialsRequest>())
    }

    @Test
    fun cachingDoesApplyToExpiredSession() {
        createSsoResponse(Instant.now().plus(2, ChronoUnit.HOURS))

        assertThat(sut.resolveCredentials()).usingRecursiveComparison().isEqualTo(credentials)

        // Resolve again
        assertThat(sut.resolveCredentials()).usingRecursiveComparison().isEqualTo(credentials)

        verify(ssoClient).getRoleCredentials(any<GetRoleCredentialsRequest>())
    }

    @Test
    fun rejectedAccessTokenInvalidatesIt() {
        ssoClient.stub {
            on(
                ssoClient.getRoleCredentials(any<GetRoleCredentialsRequest>())
            ).thenThrow(
                UnauthorizedException.builder().build()
            )
        }

        assertThatThrownBy { sut.resolveCredentials() }.isNotNull()

        verify(ssoAccessTokenProvider).invalidate()
    }

    private fun createSsoResponse(expirationTime: Instant) {
        ssoClient.stub {
            on(
                ssoClient.getRoleCredentials(
                    GetRoleCredentialsRequest.builder()
                        .accessToken(accessTokenId)
                        .accountId(accountId)
                        .roleName(roleName)
                        .build()
                )
            ).thenReturn(
                GetRoleCredentialsResponse.builder()
                    .roleCredentials(
                        RoleCredentials.builder()
                            .accessKeyId(accessKey)
                            .secretAccessKey(secretKey)
                            .sessionToken(sessionToken)
                            .expiration(expirationTime.toEpochMilli())
                            .build()
                    )
                    .build()
            )
        }
    }
}


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

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

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import software.aws.toolkits.jetbrains.core.credentials.sso.AccessToken
import java.time.Instant

class BearerTokenProviderTest {
    private val sut = { token: AccessToken? -> BearerTokenProvider.state(token) }

    @Test
    fun `state is NOT_AUTHENTICATED if there is no token`() {
        assertThat(sut(null)).isEqualTo(BearerTokenAuthState.NOT_AUTHENTICATED)
    }

    @Test
    fun `state is NOT_AUTHENTICATED if expired token doesn't have refresh token`() {
        val token = anAccessToken(null, Instant.now().minusSeconds(10))
        assertThat(sut(token)).isEqualTo(BearerTokenAuthState.NOT_AUTHENTICATED)
    }

    @Test
    fun `state is NEEDS_REFRESH if expired token has refresh token`() {
        val token = anAccessToken(expiresAt = Instant.now().minusSeconds(10))
        assertThat(sut(token)).isEqualTo(BearerTokenAuthState.NEEDS_REFRESH)
    }

    @Test
    fun `state is AUTHORIZED if token is currently valid`() {
        val token = anAccessToken(null, Instant.now().plusSeconds(10))
        assertThat(sut(token)).isEqualTo(BearerTokenAuthState.AUTHORIZED)
    }
}


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

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

import software.aws.toolkits.core.region.aRegionId
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.jetbrains.core.credentials.sso.AccessToken
import java.time.Instant

fun anAccessToken(refreshToken: String? = aString(), expiresAt: Instant) = AccessToken(
    startUrl = aString(),
    region = aRegionId(),
    accessToken = aString(),
    refreshToken = refreshToken,
    expiresAt = expiresAt
)


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

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

import com.intellij.openapi.application.ApplicationManager
import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.DisposableRule
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.junit.jupiter.api.assertThrows
import org.mockito.Mockito
import org.mockito.kotlin.any
import org.mockito.kotlin.argThat
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
import software.amazon.awssdk.core.exception.SdkException
import software.amazon.awssdk.core.interceptor.Context
import software.amazon.awssdk.core.interceptor.ExecutionAttributes
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor
import software.amazon.awssdk.core.internal.InternalCoreExecutionAttribute
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
import software.amazon.awssdk.services.ssooidc.model.AccessDeniedException
import software.amazon.awssdk.services.ssooidc.model.CreateTokenRequest
import software.amazon.awssdk.services.ssooidc.model.CreateTokenResponse
import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException
import software.aws.toolkits.core.region.aRegionId
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.MockClientManager
import software.aws.toolkits.jetbrains.core.MockClientManagerRule
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL
import software.aws.toolkits.jetbrains.core.credentials.sso.AccessToken
import software.aws.toolkits.jetbrains.core.credentials.sso.AccessTokenCacheKey
import software.aws.toolkits.jetbrains.core.credentials.sso.ClientRegistration
import software.aws.toolkits.jetbrains.core.credentials.sso.ClientRegistrationCacheKey
import software.aws.toolkits.jetbrains.core.credentials.sso.DiskCache
import java.time.Instant
import java.time.temporal.ChronoUnit

class InteractiveBearerTokenProviderTest {
    val applicationRule = ApplicationRule()
    val mockClientManager = MockClientManagerRule()

    @Rule
    @JvmField
    val ruleChain = RuleChain(
        applicationRule,
        mockClientManager
    )

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    private lateinit var oidcClient: SsoOidcClient
    private val diskCache = mock<DiskCache>()
    private val startUrl = aString()
    private val region = aRegionId()
    private val scopes = listOf("scope1", "scope2")

    @Before
    fun setUp() {
        oidcClient = mockClientManager.create<SsoOidcClient>()
    }

    @After
    fun tearDown() {
        oidcClient.close()
    }

    @Test
    fun `oidcClient retries twice on InvalidGrantException failure`() {
        fun verifyRetryAttempts(configuration: ClientOverrideConfiguration.Builder) {
            configuration.addExecutionInterceptor(
                object : ExecutionInterceptor {
                    override fun onExecutionFailure(context: Context.FailedExecution?, executionAttributes: ExecutionAttributes?) {
                        super.onExecutionFailure(context, executionAttributes)

                        // 3 total network calls, showing 4 since the sdk increments the attempt count at the beginning
                        // of the loop before it checks whether it's allowed to retry.
                        assertThat(executionAttributes?.getAttribute(InternalCoreExecutionAttribute.EXECUTION_ATTEMPT)).isEqualTo(4)
                    }
                }
            )
        }
        fun buildUnmanagedSsoOidcClientForTests(region: String): SsoOidcClient =
            AwsClientManager.getInstance()
                .createUnmanagedClient(
                    AnonymousCredentialsProvider.create(),
                    Region.of(region),
                    clientCustomizer = { _, _, _, _, configuration ->
                        verifyRetryAttempts(ssoOidcClientConfigurationBuilder(configuration))
                    }
                )

        MockClientManager.useRealImplementations(disposableRule.disposable)
        oidcClient = spy(buildUnmanagedSsoOidcClientForTests("us-east-1"))
        val registerClientResponse = oidcClient.registerClient {
            it.clientType("public")
            it.scopes(scopes)
            it.clientName("test")
        }
        val deviceAuthorizationResponse = oidcClient.startDeviceAuthorization {
            it.clientId(registerClientResponse.clientId())
            it.clientSecret(registerClientResponse.clientSecret())
            it.startUrl(SONO_URL)
        }
        assertThrows<InvalidGrantException> {
            oidcClient.createToken {
                it.clientId(registerClientResponse.clientId())
                it.clientSecret(registerClientResponse.clientSecret())
                it.deviceCode(deviceAuthorizationResponse.deviceCode() + "invalid")
                it.grantType("urn:ietf:params:oauth:grant-type:device_code")
            }
        }
    }

    @Test
    fun `resolveToken does't refresh if token was retrieved recently`() {
        stubClientRegistration()
        whenever(diskCache.loadAccessToken(any<AccessTokenCacheKey>())).thenReturn(
            AccessToken(
                startUrl = startUrl,
                region = region,
                accessToken = "accessToken",
                refreshToken = "refreshToken",
                expiresAt = Instant.now().plus(1, ChronoUnit.HOURS)
            )
        )
        val sut = buildSut()
        sut.resolveToken()
    }

    @Test
    fun `resolveToken throws if reauthentication is needed`() {
        stubClientRegistration()
        stubAccessToken()
        Mockito.reset(oidcClient)
        whenever(oidcClient.createToken(any<CreateTokenRequest>())).thenThrow(AccessDeniedException.create("denied", null))

        val sut = buildSut()
        assertThrows<SdkException> { sut.resolveToken() }
    }

    @Test
    fun `invalidate notifies listeners of update`() {
        val mockListener = mock<BearerTokenProviderListener>()
        val conn = ApplicationManager.getApplication().messageBus.connect()
        conn.subscribe(BearerTokenProviderListener.TOPIC, mockListener)

        stubClientRegistration()
        stubAccessToken()
        val sut = buildSut()
        sut.invalidate()

        verify(mockListener).onChange(sut.id)
    }

    @Test
    fun `invalidate clears correctly`() {
        stubClientRegistration()
        stubAccessToken()
        val sut = buildSut()
        sut.invalidate()

        // initial load
        verify(diskCache).loadAccessToken(any<AccessTokenCacheKey>())

        // clears out on-disk token
        verify(diskCache).invalidateAccessToken(
            argThat<AccessTokenCacheKey> {
                val (_, url, scopes) = this
                url == startUrl && scopes == this.scopes
            }
        )

        // nothing else
        verifyNoMoreInteractions(diskCache)
    }

    @Test
    fun `reauthenticate updates current token`() {
        stubClientRegistration()
        stubAccessToken()
        val sut = buildSut()

        assertThat(sut.currentToken()?.accessToken).isEqualTo("accessToken")

        // and now instead of trying to stub out the entire OIDC device flow, abuse the fact that we short-circuit and read from disk if available
        Mockito.reset(diskCache)
        whenever(diskCache.loadAccessToken(any<AccessTokenCacheKey>())).thenReturn(
            AccessToken(
                startUrl = startUrl,
                region = region,
                accessToken = "access1",
                refreshToken = "refresh1",
                expiresAt = Instant.MAX
            )
        )
        sut.reauthenticate()

        assertThat(sut.currentToken()?.accessToken).isEqualTo("access1")
    }

    @Test
    fun `reauthenticate notifies listeners of update`() {
        val mockListener = mock<BearerTokenProviderListener>()
        val conn = ApplicationManager.getApplication().messageBus.connect()
        conn.subscribe(BearerTokenProviderListener.TOPIC, mockListener)

        stubClientRegistration()
        stubAccessToken()
        val sut = buildSut()
        sut.reauthenticate()

        // once for invalidate, once after the token has been retrieved
        verify(mockListener, times(2)).onChange(sut.id)
    }

    private fun buildSut() = InteractiveBearerTokenProvider(
        startUrl = startUrl,
        region = region,
        scopes = scopes,
        cache = diskCache
    )

    private fun stubClientRegistration() {
        whenever(diskCache.loadClientRegistration(any<ClientRegistrationCacheKey>())).thenReturn(
            ClientRegistration(
                "",
                "",
                Instant.MAX
            )
        )
    }

    private fun stubAccessToken() {
        whenever(diskCache.loadAccessToken(any<AccessTokenCacheKey>())).thenReturn(
            AccessToken(
                startUrl = startUrl,
                region = region,
                accessToken = "accessToken",
                refreshToken = "refreshToken",
                expiresAt = Instant.MIN
            )
        )
        whenever(oidcClient.createToken(any<CreateTokenRequest>())).thenReturn(
            CreateTokenResponse.builder()
                .refreshToken("refresh1")
                .accessToken("access1")
                .expiresIn(1800)
                .build()
        )
    }
}


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

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

import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.RuleChain
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.mockito.kotlin.verifyNoInteractions
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
import software.aws.toolkits.core.rules.EnvironmentVariableHelper
import software.aws.toolkits.core.utils.createParentDirectories
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.core.utils.toHexString
import software.aws.toolkits.core.utils.writeText
import software.aws.toolkits.jetbrains.core.MockClientManagerRule
import java.nio.file.Path
import java.security.MessageDigest
import java.time.Instant
import java.time.format.DateTimeFormatter

class ProfileSdkTokenProviderWrapperTest {
    val applicationRule = ApplicationRule()
    val mockClientManager = MockClientManagerRule()

    @Rule
    @JvmField
    val ruleChain = RuleChain(
        applicationRule,
        mockClientManager
    )

    private lateinit var oidcClient: SsoOidcClient

    companion object {
        lateinit var testHomeDir: Path

        @ClassRule
        @JvmField
        val envVarManager = EnvironmentVariableHelper()

        @ClassRule
        @JvmField
        val tempDirRule = TemporaryFolder()

        @BeforeClass
        @JvmStatic
        fun beforeClass() {
            testHomeDir = tempDirRule.newFolder().toPath().toAbsolutePath()
            envVarManager.set("HOME", testHomeDir.toString())
            envVarManager.set("USERPROFILE", testHomeDir.toString())
        }
    }

    @Before
    fun setUp() {
        oidcClient = mockClientManager.create<SsoOidcClient>()
    }

    @Test
    @Ignore("is flaky")
    fun `currentToken retrieves from expected location`() {
        val session = aString()
        val sut = ProfileSdkTokenProviderWrapper(sessionName = session, region = "us-east-1")
        writeToken(session, expiry = Instant.now().plusSeconds(9000))

        assertThat(sut.currentToken()).isNotNull()
    }

    @Test
    fun `retrieving nonexistent current token doesn't make SDK calls`() {
        val session = aString()
        val sut = ProfileSdkTokenProviderWrapper(sessionName = session, region = "us-east-1")

        assertThat(sut.currentToken()).isNull()
        verifyNoInteractions(oidcClient)
    }

    @Test
    fun `retrieving expired current token doesn't make SDK calls`() {
        val session = aString()
        val sut = ProfileSdkTokenProviderWrapper(sessionName = session, region = "us-east-1")
        writeToken(session, Instant.now().minusSeconds(9000))

        assertThat(sut.currentToken()).isNull()
        verifyNoInteractions(oidcClient)
    }

    private fun writeToken(session: String, expiry: Instant = Instant.now()) {
        val tokenPath = testHomeDir
            .resolve(Path.of(".aws", "sso", "cache"))
            .resolve(MessageDigest.getInstance("SHA-1").digest(session.toByteArray()).toHexString() + ".json")
        tokenPath.createParentDirectories()
        tokenPath.writeText(
            // doesn't match what SDK actually uses, but we only care about a subset of fields at the moment
            // language=JSON
            """
            {
              "accessToken": "anAccessToken",
              "expiresAt": "${DateTimeFormatter.ISO_INSTANT.format(expiry)}",
              "refreshToken": "aRefreshToken",
              "region": "aRegion",
              "startUrl": "aStartUrl"
            }
            """.trimIndent()
        )
    }
}


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

package software.aws.toolkits.jetbrains.core.docker

import com.intellij.docker.dockerFile.DockerFileType
import com.intellij.docker.dockerFile.DockerLanguage
import com.intellij.docker.dockerFile.parser.DockerParserDefinition
import com.intellij.lang.LanguageParserDefinitions
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.runInEdtAndGet
import com.intellij.testFramework.runInEdtAndWait
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import software.aws.toolkits.jetbrains.utils.rules.CodeInsightTestFixtureRule
import java.io.File

class DockerfileParserTest {

    @JvmField
    @Rule
    val projectRule = CodeInsightTestFixtureRule()
    private val dockerParserDefinition = DockerParserDefinition()

    @Before
    fun setup() {
        LanguageParserDefinitions.INSTANCE.addExplicitExtension(DockerLanguage.INSTANCE, dockerParserDefinition)
    }

    @After
    fun teardown() {
        LanguageParserDefinitions.INSTANCE.removeExplicitExtension(DockerLanguage.INSTANCE, dockerParserDefinition)
    }

    @Test
    fun basicDockerfileParsing() {
        val sut = DockerfileParser(projectRule.project)
        val file = dockerfile(
            """
                FROM amazoncorretto:8
                COPY /build/libs/app.jar /home/app.jar
                EXPOSE 80
                CMD java -jar -Dserver.port=80 /home/app.jar
            """.trimIndent()
        )

        runInEdtAndWait {
            assertThat(sut.parse(file)).isEqualTo(
                DockerfileDetails(
                    "java -jar -Dserver.port=80 /home/app.jar",
                    listOf(80),
                    listOf(CopyDirective("/build/libs/app.jar", "/home/app.jar"))
                )
            )
        }
    }

    @Test
    fun understandsWorkingDirectory() {
        val sut = DockerfileParser(projectRule.project)
        val file = dockerfile(
            """
                FROM amazoncorretto:8
                WORKDIR /other
                COPY /build/libs/app.jar /home/app.jar
                COPY /build/libs/other.txt hello.txt
                WORKDIR /home
                COPY /build/libs/thing.txt thing.txt
                EXPOSE 80
                CMD java -jar -Dserver.port=80 /home/app.jar
            """.trimIndent()
        )

        runInEdtAndWait {
            assertThat(sut.parse(file)).isEqualTo(
                DockerfileDetails(
                    "java -jar -Dserver.port=80 /home/app.jar",
                    listOf(80),
                    listOf(
                        CopyDirective("/build/libs/app.jar", "/home/app.jar"),
                        CopyDirective("/build/libs/other.txt", "/other/hello.txt"),
                        CopyDirective("/build/libs/thing.txt", "/home/thing.txt")
                    )
                )
            )
        }
    }

    @Test
    fun ignoreAddCommandsAsTheyAreNotWellSupported() {
        val sut = DockerfileParser(projectRule.project)
        val file = dockerfile(
            """
                FROM amazoncorretto:8
                ADD /build/libs/thing.txt thing.txt
            """.trimIndent()
        )

        runInEdtAndWait {
            assertThat(sut.parse(file)).isEqualTo(DockerfileDetails(null, emptyList(), emptyList()))
        }
    }

    @Test
    fun incorrectlyFormattedCopyIsRecoverable() {
        val sut = DockerfileParser(projectRule.project)
        val file = dockerfile(
            """
                FROM amazoncorretto:8
                COPY
                COPY /foo
                COPY /hello /world
            """.trimIndent()
        )

        runInEdtAndWait {
            assertThat(sut.parse(file)).isEqualTo(DockerfileDetails(null, emptyList(), listOf(CopyDirective("/hello", "/world"))))
        }
    }

    @Test
    fun multiStageBuildsTakeLastFromBlockOnly() {
        val sut = DockerfileParser(projectRule.project)
        val file = dockerfile(
            """
                FROM amazoncorretto:8 as builder
                COPY . .
                CMD ./gradlew build

                FROM amazoncorretto:8
                COPY /build/libs/app.jar /home/app.jar
                EXPOSE 80
                CMD java -jar -Dserver.port=80 /home/app.jar
            """.trimIndent()
        )

        runInEdtAndWait {
            assertThat(sut.parse(file)).isEqualTo(
                DockerfileDetails(
                    "java -jar -Dserver.port=80 /home/app.jar",
                    listOf(80),
                    listOf(CopyDirective("/build/libs/app.jar", "/home/app.jar"))
                )
            )
        }
    }

    @Test
    fun relativeFromDockerfile() {
        val sut = DockerfileParser(projectRule.project)
        val file = dockerfile(
            """
                FROM amazoncorretto:8
                COPY build/libs/app.jar /home/app.jar
                WORKDIR /blah
                COPY . .
            """.trimIndent()
        )
        val directory = file.parent.path + File.separator
        runInEdtAndWait {
            assertThat(sut.parse(file)).isEqualTo(
                DockerfileDetails(
                    null,
                    emptyList(),
                    listOf(CopyDirective("${directory}build/libs/app.jar", "/home/app.jar"), CopyDirective("$directory.", "/blah/."))
                )
            )
        }
    }

    private fun dockerfile(contents: String): VirtualFile =
        runInEdtAndGet {
            projectRule.fixture.configureByText(DockerFileType.DOCKER_FILE_TYPE, contents)
        }.virtualFile
}


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

package software.aws.toolkits.jetbrains.core.docker

import com.intellij.docker.agent.DockerAgent
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

class ToolkitDockerAdapterTest {
    private lateinit var sut: ToolkitDockerAdapter
    private lateinit var agent: DockerAgent

    @Before
    fun setUp() {
        agent = mock()
        sut = ToolkitDockerAdapter(mock(), mock()).also {
            it.agent = agent
        }
    }

    @Test
    fun toLocalImageList() {
        val mocks = arrayOf(
            mockDockerApplication("sha256:nulltag", null),
            mockDockerApplication("sha256:nonetag", arrayOf("<none>:<none>")),
            mockDockerApplication("sha256:singletag", arrayOf("tag")),
            mockDockerApplication("sha256:multipletags", arrayOf("tag1", "remote:tag2")),
        )

        // can't declare mocks inline for thenReturn
        whenever(agent.getImages(null)).thenReturn(mocks)

        runBlocking {
            assertThat(sut.getLocalImages()).containsExactly(
                LocalImage("sha256:nulltag", null),
                LocalImage("sha256:nonetag", null),
                LocalImage("sha256:singletag", "tag"),
                LocalImage("sha256:multipletags", "tag1"),
                LocalImage("sha256:multipletags", "remote:tag2")
            )
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.docker

import com.intellij.docker.agent.DockerAgentImage
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

fun mockDockerApplication(imageId: String, tags: Array<String>?): DockerAgentImage {
    val mock = mock<DockerAgentImage>()
    whenever(mock.imageId).thenReturn(imageId)
    whenever(mock.imageRepoTags).thenReturn(tags)

    return mock
}


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

package software.aws.toolkits.jetbrains.core.executables

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.testFramework.ProjectRule
import com.intellij.util.text.SemVer
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct
import software.aws.toolkits.jetbrains.services.lambda.sam.SamExecutable
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata

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

    @Test
    fun gettingCommandLineWhenExecutableIsFoundWorksAndModificationsAreApplied() {
        val path = "/foo/bar"
        val name = "Testing 1-2-3"
        val commandLine = ExecutableCommon.getCommandLine(path, name)
        assertThat(commandLine.exePath).isEqualTo(path)
        assertThat(commandLine.environment.containsKey("AWS_ACCESS_KEY_ID")).isFalse
        assertThat(commandLine.environment.containsKey("AWS_SECRET_ACCESS_KEY")).isFalse
        assertThat(commandLine.environment.containsKey("AWS_SESSION_TOKEN")).isFalse
        assertThat(commandLine.environment.containsKey("AWS_TOOLING_USER_AGENT")).isFalse
        assertThat(commandLine.parentEnvironmentType).isEqualTo(GeneralCommandLine.ParentEnvironmentType.NONE)
    }

    @Test
    fun gettingCommandLineWhenExecutableIsSamExecutable() {
        val path = "/foo/bar"
        val name = "sam.cmd"
        val clientMetadata = ClientMetadata(
            productName = AWSProduct.AWS_TOOLKIT_FOR_JET_BRAINS,
            productVersion = "1.0",
            clientId = "foo",
            parentProduct = "JetBrains",
            parentProductVersion = "191",
            os = "mac",
            osVersion = "1.0"
        )
        val commandLine = ExecutableCommon.getCommandLine(path, name, SamExecutable(), clientMetadata)
        assertThat(commandLine.exePath).isEqualTo(path)
        assertThat(commandLine.environment.containsKey("AWS_TOOLING_USER_AGENT")).isTrue
    }

    @Test
    fun checkSemVerVersionThrowsIfOver() {
        val curr = SemVer("10.0.0", 10, 0, 0)
        val min = SemVer("0.0.1", 0, 0, 1)
        val max = SemVer("9.9.9", 9, 9, 9)
        val name = "high and outside"
        assertThatThrownBy { ExecutableCommon.checkSemVerVersion(curr, min, max, name) }
            .isInstanceOf(RuntimeException::class.java)
    }

    @Test
    fun checkSemVerVersionThrowsIfUnder() {
        val curr = SemVer("1.0.0", 10, 0, 0)
        val min = SemVer("1.0.1", 1, 0, 1)
        val max = SemVer("9.9.9", 9, 9, 9)
        val name = "you must be this tall to ride"
        assertThatThrownBy { ExecutableCommon.checkSemVerVersion(curr, min, max, name) }
            .isInstanceOf(RuntimeException::class.java).hasMessageContaining(name)
    }

    @Test
    fun checkSemVerVersionForParallelValidVersionsThrowsIfUnder() {
        val curr = SemVer("1.0.0", 1, 0, 0)
        val min1 = SemVer("1.0.1", 1, 0, 1)
        val max1 = SemVer("3.0.0", 3, 0, 0)
        val min2 = SemVer("4.0.1", 4, 0, 1)
        val max2 = SemVer("9.9.9", 9, 9, 9)
        val name = "Lower than minimum"
        assertThatThrownBy {
            ExecutableCommon.checkSemVerVersionForParallelValidVersions(
                curr,
                listOf(
                    ExecutableVersionRange(min1, max1),
                    ExecutableVersionRange(min2, max2)
                ),
                name
            )
        }.isInstanceOf(RuntimeException::class.java)
    }

    @Test
    fun checkSemVerVersionForParallelValidVersionsThrowsIfOver() {
        val curr = SemVer("9.0.0", 9, 0, 0)
        val min1 = SemVer("1.0.1", 1, 0, 1)
        val max1 = SemVer("3.0.0", 3, 0, 0)
        val min2 = SemVer("4.0.1", 4, 0, 1)
        val max2 = SemVer("8.8.8", 8, 8, 8)
        val name = "Higher than maximum"
        assertThatThrownBy {
            ExecutableCommon.checkSemVerVersionForParallelValidVersions(
                curr,
                listOf(
                    ExecutableVersionRange(min1, max1),
                    ExecutableVersionRange(min2, max2)
                ),
                name
            )
        }.isInstanceOf(RuntimeException::class.java)
    }

    @Test
    fun checkSemVerVersionForParallelValidVersionsThrowsIfBetweenTwoValidRanges() {
        val curr = SemVer("4.0.0", 4, 0, 0)
        val min1 = SemVer("1.0.1", 1, 0, 1)
        val max1 = SemVer("3.0.0", 3, 0, 0)
        val min2 = SemVer("4.0.1", 4, 0, 1)
        val max2 = SemVer("8.8.8", 8, 8, 8)
        val name = "Between two ranges"
        assertThatThrownBy {
            ExecutableCommon.checkSemVerVersionForParallelValidVersions(
                curr,
                listOf(
                    ExecutableVersionRange(min1, max1),
                    ExecutableVersionRange(min2, max2)
                ),
                name
            )
        }.isInstanceOf(RuntimeException::class.java)
    }
}


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

package software.aws.toolkits.jetbrains.core.executables

import com.intellij.testFramework.ProjectRule
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.assertj.core.api.ObjectAssert
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import software.aws.toolkits.core.utils.test.retryableAssert
import software.aws.toolkits.core.utils.writeText
import software.aws.toolkits.jetbrains.utils.isInstanceOf
import software.aws.toolkits.jetbrains.utils.value
import software.aws.toolkits.jetbrains.utils.wait
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Duration
import java.util.concurrent.atomic.AtomicInteger

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

    @Rule
    @JvmField
    val tempFolder = TemporaryFolder()
    private val sut = DefaultExecutableManager()

    @Test
    fun nonExistentExecutableIsNotResolved() {
        val type = DummyExecutableType("dummy")

        sut.loadState(ExecutableStateList(listOf(ExecutableState("dummy", "/foo/bar", true))))

        assertThat(sut.getExecutable(type)).wait().isCompletedWithValueMatching { it is ExecutableInstance.UnresolvedExecutable }
    }

    @Test
    fun existingExecutableIsResolvedAndValidated() {
        val executable = tempFolder.newFile()
        val type = object : DummyExecutableType("dummy"), AutoResolvable, Validatable {
            override fun resolve(): Path = executable.toPath()

            override fun validate(path: Path) {}
        }

        assertThat(sut.getExecutable(type)).wait().isCompletedWithValueMatching { (it as ExecutableInstance.Executable).executablePath == executable.toPath() }
    }

    @Test
    fun previouslyResolvedExecutablesAreCached() {
        val executable = tempFolder.newFile()
        val count = AtomicInteger(0)
        val type = object : DummyExecutableType("dummy"), AutoResolvable {
            override fun resolve(): Path {
                count.getAndIncrement()
                return executable.toPath()
            }
        }

        sut.getExecutable(type).value
        sut.getExecutable(type).value

        assertThat(count).hasValue(1)
    }

    @Test
    fun cachedVersionExpiresIfFileBeenDeleted() {
        val count = AtomicInteger(0)
        val type = object : DummyExecutableType("dummy"), AutoResolvable {
            override fun resolve(): Path {
                count.getAndIncrement()
                return tempFolder.newFile().toPath()
            }
        }

        val file = sut.getExecutable(type).value as ExecutableInstance.Executable
        assertThat(file.executablePath.toFile().delete()).isTrue()

        assertThat(sut.getExecutable(type).value).isInstanceOf<ExecutableInstance.Executable>()

        assertThat(count).hasValue(2)
    }

    @Test
    fun resolutionExceptionsArePropagated() {
        val type = object : DummyExecutableType("dummy"), AutoResolvable {
            override fun resolve(): Path {
                throw RuntimeException("blah")
            }
        }

        assertThat(sut.getExecutable(type).value).isInstanceOfSatisfying(ExecutableInstance.UnresolvedExecutable::class.java) {
            assertThat(it.validationError).endsWith("blah")
        }
    }

    @Test
    fun notConsideredAutoResolvedIfChangedOutsideOfManager() {
        val executablePath = tempFolder.newFile().toPath()
        val type = object : DummyExecutableType("dummy"), AutoResolvable {
            override fun resolve() = executablePath
        }
        sut.getExecutable(type).value
        modifyFile(executablePath)

        assertThat(sut.getExecutable(type).value).isExecutableMatching(path = executablePath, autoResolved = false)
    }

    @Test
    fun updatingExecutableRevalidates() {
        val executable = tempFolder.newFile()
        val type = object : DummyExecutableType("dummy"), AutoResolvable, Validatable {
            var runtimeException = true
            override fun resolve(): Path = executable.toPath()
            override fun validate(path: Path) {
                if (runtimeException) {
                    throw RuntimeException("blah")
                }
            }
        }

        assertThat(sut.getExecutable(type).value).isInstanceOf(ExecutableInstance.InvalidExecutable::class.java)
        type.runtimeException = false
        assertThat(sut.getExecutable(type)).wait().isCompletedWithValueMatching { (it as ExecutableInstance.Executable).executablePath == executable.toPath() }
    }

    @Test
    fun savedUnresolvedPathOverwrittenWithValid() {
        val executable = tempFolder.newFile()
        val type = object : DummyExecutableType("dummy"), AutoResolvable, Validatable {
            override fun resolve(): Path = executable.toPath()
            override fun validate(path: Path) {}
        }

        sut.loadState(ExecutableStateList(listOf(ExecutableState(type.id, "/very/invalid/path"))))
        val invalidExecutableInstance = sut.getExecutableIfPresent(type)
        assertThat(invalidExecutableInstance).isInstanceOf(ExecutableInstance.UnresolvedExecutable::class.java)
        val validExecutableInstance = sut.getExecutable(type).value
        assertThat(validExecutableInstance).isInstanceOf(ExecutableInstance.Executable::class.java)
        assertThat((validExecutableInstance as ExecutableInstance.Executable).autoResolved).isTrue()
    }

    @Test
    fun updatedExecutableNoLongerAutoResolved() {
        val executable = tempFolder.newFile()
        executable.setLastModified(0L)
        val type = object : DummyExecutableType("dummy"), AutoResolvable, Validatable {
            override fun validate(path: Path) {}
            override fun resolve(): Path? = executable.toPath()
        }

        retryableAssert(timeout = Duration.ofMillis(100), interval = Duration.ofMillis(20)) {
            assertThat(sut.getExecutable(type).value).isInstanceOfSatisfying(ExecutableInstance.Executable::class.java) {
                assertThat(it.executablePath).isEqualTo(executable.toPath())
                assertThat(it.autoResolved).isTrue()
            }
        }
        assertThat(sut.getExecutable(type).value).isInstanceOfSatisfying(ExecutableInstance.Executable::class.java) {
            assertThat(it.executablePath).isEqualTo(executable.toPath())
            assertThat(it.autoResolved).isTrue()
        }
        retryableAssert(timeout = Duration.ofMillis(100), interval = Duration.ofMillis(20)) {
            assertThat(sut.setExecutablePath(type, executable.toPath()).value).isInstanceOfSatisfying(ExecutableInstance.Executable::class.java) {
                assertThat(it.executablePath).isEqualTo(executable.toPath())
                assertThat(it.autoResolved).isFalse()
            }
        }
    }

    @Test
    fun validationExceptionsArePropagated() {
        val executable = tempFolder.newFile()

        val type = object : DummyExecutableType("dummy"), Validatable {
            override fun validate(path: Path) {
                throw RuntimeException("blah")
            }
        }

        sut.loadState(ExecutableStateList(listOf(ExecutableState(type.id, executable.absolutePath))))

        assertThat(sut.getExecutable(type).value).isInstanceOfSatisfying(ExecutableInstance.InvalidExecutable::class.java) {
            assertThat(it.executablePath).isEqualTo(executable.toPath())
            assertThat(it.validationError).endsWith("blah")
        }
    }

    @Test
    fun validationStatusIsCachedUntilFileChanges() {
        val executable = tempFolder.newFile().toPath()
        val count = AtomicInteger(0)

        val type = object : DummyExecutableType("dummy"), AutoResolvable, Validatable {
            override fun resolve(): Path = executable

            override fun validate(path: Path) {
                count.incrementAndGet()
            }
        }

        sut.getExecutable(type).value
        sut.getExecutable(type).value

        assertThat(count).hasValue(1)

        modifyFile(executable)

        sut.getExecutable(type).value
        assertThat(count).hasValue(2)
    }

    @Test
    fun getIfPresentRevalidatesExecutable() {
        val executable = tempFolder.newFile()
        val type = object : DummyExecutableType("dummy"), AutoResolvable, Validatable {
            override fun resolve(): Path = executable.toPath()
            override fun validate(path: Path) {}
        }

        sut.loadState(ExecutableStateList(listOf(ExecutableState(type.id, "/very/invalid/path"))))
        assertThat(sut.getExecutableIfPresent(type)).isInstanceOf(ExecutableInstance.UnresolvedExecutable::class.java)
        retryableAssert(maxAttempts = 20, interval = Duration.ofMillis(10)) {
            assertThat(sut.getExecutableIfPresent(type)).isInstanceOfSatisfying(ExecutableInstance.Executable::class.java) {
                assertThat(it.executablePath).isEqualTo(executable.toPath())
            }
        }
    }

    @Test
    fun canGetExecutableSynchronouslyOnlyIfPresent() {
        val type = DummyExecutableType("dummy")
        val executable = tempFolder.newFile()

        sut.setExecutablePath(type, executable.toPath()).value

        assertThat(sut.getExecutableIfPresent(type)).isInstanceOfSatisfying(ExecutableInstance.Executable::class.java) {
            assertThat(it.executablePath).isEqualTo(executable.toPath())
        }
    }

    @Test
    fun setExecutablePathFailsWhenValidateFails() {
        val type = object : DummyExecutableType("dummy"), AutoResolvable, Validatable {
            override fun resolve(): Path? = null
            override fun validate(path: Path) {
                throw RuntimeException("ow")
            }
        }
        val executable = "/fake/path////////////"

        sut.setExecutablePath(type, Paths.get(executable)).value

        // path does not exist so it's unresolved
        assertThat(sut.getExecutable(type).value).isInstanceOf(ExecutableInstance.UnresolvedExecutable::class.java)
    }

    @Test
    fun executableTypeJavaGetExecutableThrowsWhenNotRegistered() {
        val type = DummyExecutableType("dummy")
        val executable = tempFolder.newFile()

        sut.setExecutablePath(type, executable.toPath()).value

        assertThatThrownBy {
            ExecutableType.getExecutable(type.javaClass)
        }
    }

    private fun modifyFile(executable: Path) {
        Thread.sleep(1000) // Path.lastModified() is only second-level granularity
        executable.writeText("dummy")
    }

    private fun ObjectAssert<ExecutableInstance>.isExecutableMatching(version: String? = null, path: Path? = null, autoResolved: Boolean? = null) =
        isInstanceOfSatisfying(ExecutableInstance.Executable::class.java) { executable ->
            version?.let { assertThat(executable.version).isEqualTo(it) }
            path?.let { assertThat(executable.executablePath).isEqualTo(it) }
            autoResolved?.let { assertThat(executable.autoResolved).isEqualTo(autoResolved) }
        }

    private open class DummyExecutableType(final override val id: String, private val version: () -> String = { "1.4" }) : ExecutableType<String> {
        override val displayName = id

        override fun version(path: Path): String = version()
    }
}


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

package software.aws.toolkits.jetbrains.core.execution

import com.intellij.execution.RunManager
import com.intellij.execution.application.ApplicationConfiguration
import com.intellij.execution.application.ApplicationConfigurationType
import com.intellij.testFramework.ProjectRule
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.ObjectAssert
import org.junit.Rule
import org.junit.Test
import software.aws.toolkits.jetbrains.settings.AwsSettingsRule

class AwsConnectionExtensionSettingsEditorTest {

    @Rule
    @JvmField
    val projectRule = ProjectRule()

    @Rule
    @JvmField
    val settingsRule = AwsSettingsRule()

    @Test
    fun baseState() {
        val editor = AwsConnectionExtensionSettingsEditor<ApplicationConfiguration>(projectRule.project, false)

        assertThat(editor.view.none.isSelected).isTrue()

        assertThat(editor.view.credentialProvider.isEnabled).isFalse()
        assertThat(editor.view.credentialProvider.itemCount).isZero() // We don't want to eagerly load for every RunConfiguration

        assertThat(editor.view.region.isEnabled).isFalse()
        assertThat(editor.view.region.itemCount).isZero() // We don't want to eagerly load for every RunConfiguration
    }

    @Test
    fun canRoundTripUseCurrentConnection() {
        val configuration = createConfiguration {
            useCurrentConnection = true
        }

        val editor = AwsConnectionExtensionSettingsEditor<ApplicationConfiguration>(projectRule.project, false)

        editor.resetFrom(configuration)

        assertThat(editor.view.useCurrentConnection.isSelected).isTrue()

        assertThat(editor.view.credentialProvider.isEnabled).isFalse()
        assertThat(editor.view.credentialProvider.itemCount).isZero() // We don't want to eagerly load for every RunConfiguration
        assertThat(editor.view.region.isEnabled).isFalse()
        assertThat(editor.view.region.itemCount).isZero() // We don't want to eagerly load for every RunConfiguration

        assertThat(editor).isPersistedAs {
            useCurrentConnection = true
            region = null
            credential = null
        }
    }

    @Test
    fun canLoadSpecificManualSelection() {
        val configuration = createConfiguration {
            useCurrentConnection = false
            region = "us-east-1"
            credential = "DUMMY"
        }

        val editor = AwsConnectionExtensionSettingsEditor<ApplicationConfiguration>(projectRule.project, false)

        editor.resetFrom(configuration)

        assertThat(editor.view.manuallyConfiguredConnection.isSelected).isTrue()
        assertThat(editor.view.region.isEnabled).isTrue()
        assertThat(editor.view.region.itemCount).isGreaterThan(0)
        assertThat(editor.view.credentialProvider.isEnabled).isTrue()
        assertThat(editor.view.credentialProvider.itemCount).isGreaterThan(0)

        assertThat(editor).isPersistedAs {
            useCurrentConnection = false
            region = "us-east-1"
            credential = "DUMMY"
        }
    }

    @Test
    fun canLoadNone() {
        val configuration = createConfiguration { }

        val editor = AwsConnectionExtensionSettingsEditor<ApplicationConfiguration>(projectRule.project, false)

        editor.resetFrom(configuration)

        assertThat(editor.view.none.isSelected).isTrue()

        assertThat(editor.view.credentialProvider.isEnabled).isFalse()
        assertThat(editor.view.credentialProvider.itemCount).isZero() // We don't want to eagerly load for every RunConfiguration

        assertThat(editor.view.region.isEnabled).isFalse()
        assertThat(editor.view.region.itemCount).isZero() // We don't want to eagerly load for every RunConfiguration

        assertThat(editor).isPersistedAs {
            useCurrentConnection = false
            region = null
            credential = null
        }
    }

    @Test
    fun manualConnectionEnablesDropDowns() {
        val editor = AwsConnectionExtensionSettingsEditor<ApplicationConfiguration>(projectRule.project, false)
        editor.view.manuallyConfiguredConnection.doClick()

        assertThat(editor.view.region.isEnabled).isTrue()
        assertThat(editor.view.credentialProvider.isEnabled).isTrue()
    }

    private fun createConfiguration(optionBuilder: AwsCredentialInjectionOptions.() -> Unit): ApplicationConfiguration {
        val runManager = RunManager.getInstance(projectRule.project)
        val configuration = runManager.createConfiguration("test", ApplicationConfigurationType::class.java).configuration as ApplicationConfiguration
        configuration.putCopyableUserData(AWS_CONNECTION_RUN_CONFIGURATION_KEY, AwsCredentialInjectionOptions().apply(optionBuilder))
        return configuration
    }

    private fun ApplicationConfiguration.extensionOptions() = getCopyableUserData(AWS_CONNECTION_RUN_CONFIGURATION_KEY)

    private fun ObjectAssert<AwsConnectionExtensionSettingsEditor<ApplicationConfiguration>>.isPersistedAs(
        expected: AwsCredentialInjectionOptions.() -> Unit
    ) {
        satisfies {
            val updatedConfiguration = createConfiguration { }
            it.applyTo(updatedConfiguration)
            assertThat(updatedConfiguration.extensionOptions()).usingRecursiveComparison().isEqualTo(AwsCredentialInjectionOptions(expected))
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.execution

import com.intellij.execution.RunManager
import com.intellij.execution.application.ApplicationConfiguration
import com.intellij.execution.application.ApplicationConfigurationType
import com.intellij.testFramework.ProjectRule
import org.assertj.core.api.Assertions.assertThat
import org.jdom.Element
import org.jetbrains.plugins.gradle.service.execution.GradleRunConfiguration
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.mock
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.aws.toolkits.jetbrains.core.credentials.MockCredentialsManager
import software.aws.toolkits.jetbrains.core.region.MockRegionProviderRule
import software.aws.toolkits.jetbrains.settings.AwsSettingsRule
import software.aws.toolkits.jetbrains.utils.rules.ExperimentRule

class JavaAwsConnectionExtensionTest {

    @Rule
    @JvmField
    val projectRule = ProjectRule()

    @Rule
    @JvmField
    val settingsRule = AwsSettingsRule()

    @Rule
    @JvmField
    val regionProvider = MockRegionProviderRule()

    @Rule
    @JvmField
    val registryRule = ExperimentRule(JavaAwsConnectionExperiment)

    private val mockCreds = AwsBasicCredentials.create("Access", "ItsASecret")

    @Before
    fun setUp() {
        MockCredentialsManager.getInstance().addCredentials("MockCredentials", mockCreds)
    }

    @Test
    fun `Round trip persistence`() {
        val runManager = RunManager.getInstance(projectRule.project)
        val configuration = runManager.createConfiguration("test", ApplicationConfigurationType::class.java).configuration as ApplicationConfiguration

        val data = AwsCredentialInjectionOptions {
            region = "abc123"
            credential = "mockCredential"
        }

        configuration.putCopyableUserData(AWS_CONNECTION_RUN_CONFIGURATION_KEY, data)
        configuration.mainClassName = "com.bla.Boop"

        val element = Element("bling")
        configuration.writeExternal(element)

        val deserialized = runManager.createConfiguration("re-read", ApplicationConfigurationType::class.java).configuration as ApplicationConfiguration
        deserialized.readExternal(element)

        assertThat(deserialized.mainClassName).isEqualTo("com.bla.Boop")
        assertThat(deserialized.getCopyableUserData(AWS_CONNECTION_RUN_CONFIGURATION_KEY)).usingRecursiveComparison().isEqualTo(data)
    }

    @Test
    fun `ignores gradle based run configs`() {
        val configuration = mock<GradleRunConfiguration>().apply {
            putCopyableUserData<AwsCredentialInjectionOptions>(
                AWS_CONNECTION_RUN_CONFIGURATION_KEY,
                AwsCredentialInjectionOptions {
                    region = "abc123"
                    credential = "mockCredential"
                }
            )
        }

        assertThat(JavaAwsConnectionExtension().isApplicableFor(configuration)).isFalse()
    }

    @Test
    fun `Does not inject by default`() {
        val runManager = RunManager.getInstance(projectRule.project)
        val configuration = runManager.createConfiguration("test", ApplicationConfigurationType::class.java).configuration as ApplicationConfiguration
        assertThat(configuration.getCopyableUserData(AWS_CONNECTION_RUN_CONFIGURATION_KEY)).isNull()
    }

    @Test
    fun `Does not throw on default options`() {
        val runManager = RunManager.getInstance(projectRule.project)
        val configuration = runManager.createConfiguration("test", ApplicationConfigurationType::class.java).configuration as ApplicationConfiguration
        configuration.putCopyableUserData(AWS_CONNECTION_RUN_CONFIGURATION_KEY, AwsCredentialInjectionOptions.DEFAULT_OPTIONS)
        val extension = JavaAwsConnectionExtension()
        val map = mutableMapOf<String, String>()
        extension.updateJavaParameters(configuration, mock { on { env } doAnswer { map } }, null)
    }

    @Test
    fun `Inject injects environment variables`() {
        val runManager = RunManager.getInstance(projectRule.project)
        val configuration = runManager.createConfiguration("test", ApplicationConfigurationType::class.java).configuration as ApplicationConfiguration
        val data = AwsCredentialInjectionOptions {
            region = regionProvider.defaultRegion().id
            credential = "MockCredentials"
        }
        configuration.putCopyableUserData(AWS_CONNECTION_RUN_CONFIGURATION_KEY, data)

        val extension = JavaAwsConnectionExtension()
        val map = mutableMapOf<String, String>()
        extension.updateJavaParameters(configuration, mock { on { env } doAnswer { map } }, null)
        assertThat(map).hasSize(4)
        listOf(
            "AWS_ACCESS_KEY_ID",
            "AWS_REGION",
            "AWS_DEFAULT_REGION",
            "AWS_SECRET_ACCESS_KEY"
        ).forEach { assertThat(map[it]).isNotNull() }
    }
}


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

package software.aws.toolkits.jetbrains.core.experiments

import com.intellij.openapi.application.ApplicationManager
import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ExtensionTestUtil
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import software.aws.toolkits.core.rules.SystemPropertyHelper
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.jetbrains.core.experiments.ToolkitExperimentManager.Companion.EXPERIMENT_CHANGED
import software.aws.toolkits.jetbrains.utils.deserializeState
import software.aws.toolkits.jetbrains.utils.rules.RegistryRule
import software.aws.toolkits.jetbrains.utils.serializeState
import java.time.Duration
import java.time.Instant

class ToolkitExperimentManagerTest {

    @JvmField
    @Rule
    val disposableRule = DisposableRule()

    @JvmField
    @Rule
    val applicationRule = ApplicationRule()

    @JvmField
    @Rule
    val systemPropertyHelper = SystemPropertyHelper()

    @JvmField
    @Rule
    val developerModeRule = RegistryRule("aws.toolkit.developerMode", desiredEnabledState = false)

    @Test
    fun `experiments can be enabled by system property`() {
        val experiment = DummyExperiment()

        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)

        assertThat(experiment.isEnabled()).isFalse

        System.setProperty("aws.experiment.${experiment.id}", "")
        assertThat(experiment.isEnabled()).isTrue

        System.setProperty("aws.experiment.${experiment.id}", " ")
        assertThat(experiment.isEnabled()).isTrue

        System.setProperty("aws.experiment.${experiment.id}", "true")
        assertThat(experiment.isEnabled()).isTrue

        System.setProperty("aws.experiment.${experiment.id}", "True")
        assertThat(experiment.isEnabled()).isTrue

        System.setProperty("aws.experiment.${experiment.id}", "TRUE")
        assertThat(experiment.isEnabled()).isTrue

        System.setProperty("aws.experiment.${experiment.id}", "false")
        assertThat(experiment.isEnabled()).isFalse

        System.setProperty("aws.experiment.${experiment.id}", "foobar")
        assertThat(experiment.isEnabled()).isFalse
    }

    @Test
    fun `only registered experiments can be enabled`() {
        val registered = DummyExperiment()
        val notRegistred = DummyExperiment()

        val sut = ToolkitExperimentManager.getInstance()
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(registered), disposableRule.disposable)

        sut.setState(registered, enabled = true)
        sut.setState(notRegistred, enabled = true)

        assertThat(registered.isEnabled()).isTrue
        assertThat(notRegistred.isEnabled()).isFalse
    }

    @Test
    fun `hidden experiments are not considered visible`() {
        val regular = DummyExperiment()
        val hidden = DummyExperiment(hidden = true)

        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(regular, hidden), disposableRule.disposable)

        assertThat(ToolkitExperimentManager.visibleExperiments()).containsOnly(regular)
    }

    @Test
    fun `experiments can be enabled by default`() {
        val experiment = DummyExperiment(default = true)
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        assertThat(experiment.isEnabled()).isTrue
    }

    @Test
    fun `experiments enabled by default can be disabled`() {
        val experiment = DummyExperiment(default = true)
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        experiment.setState(false)
        assertThat(experiment.isEnabled()).isFalse
    }

    @Test
    fun `state only stored if it differs from default, allowing a previously released experiment to become enabled by default`() {
        val experiment = DummyExperiment()
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        experiment.setState(true)

        assertThat(ToolkitExperimentManager.getInstance().state.value).containsEntry(experiment.id, true)
        experiment.setState(false)
        assertThat(ToolkitExperimentManager.getInstance().state.value).doesNotContainKey(experiment.id)
    }

    @Test
    fun `experiments are considered equal based on id`() {
        val first = DummyExperiment()
        val second = DummyExperiment(id = first.id)

        assertThat(first === second).isFalse
        assertThat(first).isEqualTo(second)
    }

    @Test
    fun `all experiments are enabled in developer mode`() {
        val experiment = DummyExperiment()

        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        developerModeRule.setState(true)

        assertThat(experiment.isEnabled()).isTrue
    }

    @Test
    fun `explicit enable or disable takes precedence over dev mode and system property`() {
        val experiment = DummyExperiment()
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        developerModeRule.setState(true)
        experiment.setState(false)
        System.setProperty("aws.experiment.${experiment.id}", "true")

        assertThat(experiment.isEnabled()).isFalse
    }

    @Test
    fun `system property takes precedence over dev mode`() {
        val experiment = DummyExperiment()
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        developerModeRule.setState(true)
        System.setProperty("aws.experiment.${experiment.id}", "false")

        assertThat(experiment.isEnabled()).isFalse
    }

    @Test
    fun `it correctly persists`() {
        val experiment = DummyExperiment()
        val permanentlySuppressed = DummyExperiment()
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        val now = Instant.now()

        val sut = ToolkitExperimentManager.getInstance()

        sut.shouldPrompt(experiment, now)
        sut.neverPrompt(permanentlySuppressed)
        experiment.setState(true)
        val serialized = serializeState("experiments", sut)

        val other = ToolkitExperimentManager()
        deserializeState(serialized, other)

        assertThat(other.isEnabled(experiment)).isTrue
        assertThat(other.shouldPrompt(experiment)).isFalse
        assertThat(other.shouldPrompt(experiment, now.minusMillis(5))).isFalse
    }

    @Test
    fun `event publishing - enabling experiments will emit event`() {
        val anExperiment = DummyExperiment()
        assertThat(anExperiment.isEnabled()).isFalse
        val mockListener: ToolkitExperimentStateChangedListener = mock()
        subscribeToTopic(mockListener)
        anExperiment.setState(true)

        argumentCaptor<ToolkitExperiment>().apply {
            verify(mockListener).enableSettingsStateChanged(capture())
            assertThat(allValues).hasSize(1)
            assertThat(firstValue.id).isEqualTo(anExperiment.id)
        }
    }

    @Test
    fun `event publishing - disabling experiments will emit event`() {
        val experiment = DummyExperiment(default = true)
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        assertThat(experiment.isEnabled()).isTrue
        val mockListener: ToolkitExperimentStateChangedListener = mock()
        subscribeToTopic(mockListener)
        experiment.setState(false)

        argumentCaptor<ToolkitExperiment>().apply {
            verify(mockListener).enableSettingsStateChanged(capture())
            assertThat(allValues).hasSize(1)
            assertThat(firstValue.id).isEqualTo(experiment.id)
        }
    }

    @Test
    fun `updated experiment state is reflected when event is consumed`() {
        val anExperiment = DummyExperiment()
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(anExperiment), disposableRule.disposable)
        subscribeToTopic { toolkitExperiment -> assertThat(toolkitExperiment.isEnabled()).isTrue }
        anExperiment.setState(true)
    }

    @Test
    fun `event publishing - setting experiments ineffectively will not emit message - true`() {
        val experiment = DummyExperiment(default = true)
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        assertThat(experiment.isEnabled()).isTrue
        val mockListener: ToolkitExperimentStateChangedListener = mock()
        subscribeToTopic(mockListener)

        experiment.setState(true)

        verifyNoInteractions(mockListener)
    }

    @Test
    fun `event publishing - setting experiments ineffectively will not emit message - false`() {
        val experiment = DummyExperiment(default = false)
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        assertThat(experiment.isEnabled()).isFalse
        val mockListener: ToolkitExperimentStateChangedListener = mock()
        subscribeToTopic(mockListener)

        experiment.setState(false)
        verifyNoInteractions(mockListener)
    }

    @Test
    fun `experiment prompts - experiment never seen before will prompt`() {
        val experiment = DummyExperiment()
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)

        val sut = ToolkitExperimentManager.getInstance()

        assertThat(sut.shouldPrompt(experiment)).isTrue
    }

    @Test
    fun `experiment prompts - when experiment enabled return false`() {
        val experiment = DummyExperiment()
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)

        val sut = ToolkitExperimentManager.getInstance()
        experiment.setState(true)

        assertThat(sut.shouldPrompt(experiment)).isFalse
    }

    @Test
    fun `experiment prompts - prompts are snoozed for the specified duration`() {
        val experiment = DummyExperiment()
        val now = Instant.now()
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)

        val sut = ToolkitExperimentManager.getInstance()

        assertThat(sut.shouldPrompt(experiment, now)).isTrue
        assertThat(sut.shouldPrompt(experiment, now)).isFalse
        assertThat(sut.shouldPrompt(experiment, now.plusMillis(2))).isTrue
    }

    @Test
    fun `experiment prompts - can be permanently snoozed`() {
        val experiment = DummyExperiment()
        val now = Instant.now()
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)

        val sut = ToolkitExperimentManager.getInstance()

        sut.neverPrompt(experiment)
        assertThat(sut.shouldPrompt(experiment, now.plusMillis(2))).isFalse
    }

    private fun subscribeToTopic(listener: ToolkitExperimentStateChangedListener) {
        val conn = ApplicationManager.getApplication().messageBus.connect(disposableRule.disposable)
        conn.subscribe(EXPERIMENT_CHANGED, listener)
    }

    @Test
    fun `Enabling experiments will emit event`() {
        val anExperiment = DummyExperiment()
        assertThat(anExperiment.isEnabled()).isFalse
        val mockListener: ToolkitExperimentStateChangedListener = mock()
        val conn = ApplicationManager.getApplication().messageBus.connect()
        conn.subscribe(ToolkitExperimentManager.EXPERIMENT_CHANGED, mockListener)
        anExperiment.setState(true)

        argumentCaptor<ToolkitExperiment>().apply {
            verify(mockListener).enableSettingsStateChanged(capture())
            assertThat(allValues).hasSize(1)
            assertThat(firstValue.id).isEqualTo(anExperiment.id)
        }

        conn.dispose()
    }

    @Test
    fun `Disabling experiments will emit event`() {
        val experiment = DummyExperiment(default = true)
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        assertThat(experiment.isEnabled()).isTrue
        val mockListener: ToolkitExperimentStateChangedListener = mock()
        val conn = ApplicationManager.getApplication().messageBus.connect()
        conn.subscribe(ToolkitExperimentManager.EXPERIMENT_CHANGED, mockListener)
        experiment.setState(false)

        argumentCaptor<ToolkitExperiment>().apply {
            verify(mockListener).enableSettingsStateChanged(capture())
            assertThat(allValues).hasSize(1)
            assertThat(firstValue.id).isEqualTo(experiment.id)
        }

        conn.dispose()
    }

    @Test
    fun `Setting experiments ineffectively will not emit message - true`() {
        val experiment = DummyExperiment(default = true)
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        assertThat(experiment.isEnabled()).isTrue
        val mockListener: ToolkitExperimentStateChangedListener = mock()
        val conn = ApplicationManager.getApplication().messageBus.connect()
        conn.subscribe(ToolkitExperimentManager.EXPERIMENT_CHANGED, mockListener)

        experiment.setState(true)
        verifyNoInteractions(mockListener)

        conn.dispose()
    }

    @Test
    fun `Setting experiments ineffectively will not emit message - false`() {
        val experiment = DummyExperiment(default = false)
        ExtensionTestUtil.maskExtensions(ToolkitExperimentManager.EP_NAME, listOf(experiment), disposableRule.disposable)
        assertThat(experiment.isEnabled()).isFalse
        val mockListener: ToolkitExperimentStateChangedListener = mock()
        val conn = ApplicationManager.getApplication().messageBus.connect()
        conn.subscribe(ToolkitExperimentManager.EXPERIMENT_CHANGED, mockListener)

        experiment.setState(false)
        verifyNoInteractions(mockListener)

        conn.dispose()
    }
}

class DummyExperiment(
    id: String = aString(),
    hidden: Boolean = false,
    default: Boolean = false,
    suggestionSnooze: Duration = Duration.ofMillis(1)
) : ToolkitExperiment(id, { "Dummy ($id)" }, { "Dummy Description" }, hidden, default, suggestionSnooze)


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.ide.projectView.PresentationData
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.util.Ref
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ExtensionTestUtil
import com.intellij.testFramework.ProjectRule
import com.intellij.ui.tree.AsyncTreeModel
import com.intellij.ui.tree.StructureTreeModel
import com.intellij.ui.treeStructure.Tree
import com.intellij.util.concurrency.Invoker
import com.intellij.util.ui.tree.TreeUtil
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.atLeastOnce
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import software.aws.toolkits.jetbrains.core.MockResourceCacheRule
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode
import software.aws.toolkits.jetbrains.core.fillResourceCache
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import javax.swing.tree.TreeModel

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

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    @JvmField
    @Rule
    val resourceCache = MockResourceCacheRule()

    @Before
    fun setUp() {
        resourceCache.fillResourceCache(projectRule.project)
    }

    @Test
    fun testNodePostProcessorIsInvoked() {
        val mockExtension = mock<AwsExplorerNodeProcessor>()

        ExtensionTestUtil.maskExtensions(AwsExplorerNodeProcessor.EP_NAME, listOf(mockExtension), disposableRule.disposable)

        val countDownLatch = CountDownLatch(1)

        runInEdt {
            TreeUtil.expand(Tree(createTreeModel()), 1) {
                countDownLatch.countDown()
            }
        }

        countDownLatch.await(1, TimeUnit.SECONDS)

        verify(mockExtension, atLeastOnce()).postProcessPresentation(any(), any())
    }

    @Test
    fun testNodesArePostProcessedInBackground() {
        val ranOnCorrectThread = Ref(true)
        val ran = Ref(false)

        ExtensionTestUtil.maskExtensions(
            AwsExplorerNodeProcessor.EP_NAME,
            listOf(
                object : AwsExplorerNodeProcessor {
                    override fun postProcessPresentation(node: AwsExplorerNode<*>, presentation: PresentationData) {
                        ran.set(true)
                        ran.set(ranOnCorrectThread.get() && !ApplicationManager.getApplication().isDispatchThread)
                    }
                }
            ),
            disposableRule.disposable
        )

        val countDownLatch = CountDownLatch(1)

        runInEdt {
            TreeUtil.expand(Tree(createTreeModel()), 1) {
                countDownLatch.countDown()
            }
        }

        countDownLatch.await(1, TimeUnit.SECONDS)

        assertThat(ran.get()).isTrue()
        assertThat(ranOnCorrectThread.get()).isTrue()
    }

    private fun createTreeModel(): TreeModel {
        val awsTreeModel = AwsExplorerTreeStructure(projectRule.project)
        val structureTreeModel = StructureTreeModel(
            awsTreeModel,
            null,
            Invoker.forBackgroundPoolWithoutReadAction(disposableRule.disposable),
            disposableRule.disposable
        )
        return AsyncTreeModel(structureTreeModel, false, disposableRule.disposable)
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.openapi.application.runInEdt
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ExtensionTestUtil
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.runInEdtAndWait
import com.intellij.ui.tree.AsyncTreeModel
import com.intellij.ui.tree.StructureTreeModel
import com.intellij.ui.treeStructure.Tree
import com.intellij.util.concurrency.Invoker
import com.intellij.util.ui.tree.TreeUtil
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import software.aws.toolkits.jetbrains.core.MockResourceCacheRule
import software.aws.toolkits.jetbrains.core.fillResourceCache
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import javax.swing.tree.TreeModel

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

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    @JvmField
    @Rule
    val resourceCache = MockResourceCacheRule()

    @Before
    fun setUp() {
        resourceCache.fillResourceCache(projectRule.project)
    }

    @Test
    fun testTreeStructureProviderIsInvoked() {
        val mockExtension = mock<AwsExplorerTreeStructureProvider>()
        ExtensionTestUtil.maskExtensions(AwsExplorerTreeStructureProvider.EP_NAME, listOf(mockExtension), disposableRule.disposable)

        val countDownLatch = CountDownLatch(1)

        val model = Tree(createTreeModel())

        runInEdt {
            TreeUtil.expand(model, 1) {
                countDownLatch.countDown()
            }
        }

        countDownLatch.await(1, TimeUnit.SECONDS)

        verify(mockExtension, atLeastOnce()).modify(any(), any(), any())

        runInEdtAndWait {
            TreeUtil.collapseAll(model, 0)
        }
    }

    private fun createTreeModel(): TreeModel {
        val awsTreeModel = AwsExplorerTreeStructure(projectRule.project)
        val structureTreeModel =
            StructureTreeModel(awsTreeModel, null, Invoker.forBackgroundThreadWithoutReadAction(disposableRule.disposable), disposableRule.disposable)
        return AsyncTreeModel(structureTreeModel, false, disposableRule.disposable)
    }
}


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

package software.aws.toolkits.jetbrains.core.notification

import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.actionSystem.DataKey
import com.intellij.openapi.actionSystem.impl.SimpleDataContext
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.BuildNumber
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.TestActionEvent
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.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import software.aws.toolkits.core.rules.SystemPropertyHelper
import software.aws.toolkits.jetbrains.utils.rules.NotificationListenerRule
import java.util.Calendar

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

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    @Rule
    @JvmField
    val systemProperties = SystemPropertyHelper()

    @Rule
    @JvmField
    val notifications = NotificationListenerRule(projectRule, disposableRule.disposable)

    private lateinit var sut: MinimumVersionChange
    private lateinit var applicationInfo: ApplicationInfo

    @Before
    fun setup() {
        applicationInfo = mock {
            on { buildDate } doReturn Calendar.getInstance() // Avoids a NPE in IdeaFormatWriter, doesn't hurt but spams logs
        }
        ApplicationManager.getApplication().replaceService(ApplicationInfo::class.java, applicationInfo, disposableRule.disposable)

        sut = MinimumVersionChange(isUnderTest = true)

        PropertiesComponent.getInstance().unsetValue(MinimumVersionChange.IGNORE_PROMPT)
    }

    @Before
    fun tearDown() {
        PropertiesComponent.getInstance().unsetValue(MinimumVersionChange.IGNORE_PROMPT)
    }

    @Test
    fun `notice is hidden if system property is set`() {
        System.setProperty(MinimumVersionChange.SKIP_PROMPT, "true")
        sut.runActivity(projectRule.project)

        assertThat(notifications.notifications).isEmpty()
    }

    @Test
    fun `notice is hidden if suppressed before`() {
        PropertiesComponent.getInstance().setValue(MinimumVersionChange.IGNORE_PROMPT, true)
        sut.runActivity(projectRule.project)

        assertThat(notifications.notifications).isEmpty()
    }

    @Test
    fun `notice is hidden if IDE is above max version`() {
        applicationInfo.stub {
            on { build } doReturn BuildNumber("IC", 999, 999)
        }

        sut.runActivity(projectRule.project)

        assertThat(notifications.notifications).isEmpty()
    }

    @Test
    fun `notice is hidden if IDE is at min version version`() {
        applicationInfo.stub {
            on { build } doReturn BuildNumber("IC", MinimumVersionChange.MIN_VERSION, 999)
        }

        sut.runActivity(projectRule.project)

        assertThat(notifications.notifications).isEmpty()
    }

    @Test
    fun `notice is shown if IDE is below min version`() {
        applicationInfo.stub {
            on { build } doReturn BuildNumber("IC", 123, 456)
            on { fullVersion } doReturn "2012.3"
        }

        sut.runActivity(projectRule.project)

        assertThat(notifications.notifications).singleElement().satisfies {
            assertThat(it.content).matches("""Support for [\w ]+ 2012\.3 is being deprecated .*""".toPattern())
            assertThat(it.actions).singleElement()
        }
    }

    @Test
    fun `don't show again sets the setting`() {
        applicationInfo.stub {
            on { build } doReturn BuildNumber("IC", 123, 456)
            on { fullVersion } doReturn "2032.1"
        }

        sut.runActivity(projectRule.project)

        // While this code should never throw...the DateKey for the Notification is private, so we can't make this logic 100% safe forever.
        // The logic that makes use of it is to dismiss the balloon and runs after our logic, so it should be safe.
        runCatching {
            val notification = notifications.notifications.first()
            val context = SimpleDataContext.builder()
                .add(DataKey.create("Notification"), notification)
                .build()
            val actionEvent = TestActionEvent(context)
            notification.actions.first().actionPerformed(actionEvent)
        }

        assertThat(PropertiesComponent.getInstance().getBoolean(MinimumVersionChange.IGNORE_PROMPT)).isTrue
    }
}