Skip to content

Commit

Permalink
Merge pull request #91 from VAuthenticator/make-client-application-re…
Browse files Browse the repository at this point in the history
…pository-cacheble

Make client application repository cacheble
  • Loading branch information
mrFlick72 authored Jan 6, 2023
2 parents 5f161f8 + 9b5c7ce commit ced7acb
Show file tree
Hide file tree
Showing 13 changed files with 290 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ class AccountConfig {
@Bean
fun accountCacheOperation(
redisTemplate: RedisTemplate<*, *>,
@Value("\${vauthenticator.dynamo-db.account.cache.ttl}") accountCacheTtl: Duration,
@Value("\${vauthenticator.dynamo-db.account.cache.name}") accountCacheRegionName: String,
@Value("\${vauthenticator.dynamo-db.account.cache.ttl}") ttl: Duration,
@Value("\${vauthenticator.dynamo-db.account.cache.name}") cacheRegionName: String,
) = RedisCacheOperation<String, String>(
cacheName = accountCacheRegionName,
ttl = accountCacheTtl,
cacheName = cacheRegionName,
ttl = ttl,
redisTemplate = redisTemplate as RedisTemplate<String, String>
)
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,54 @@
package it.valeriovaudi.vauthenticator.config

import it.valeriovaudi.vauthenticator.oauth2.clientapp.ClientApplicationRepository
import it.valeriovaudi.vauthenticator.oauth2.clientapp.DynamoDbClientApplicationRepository
import it.valeriovaudi.vauthenticator.oauth2.clientapp.ReadClientApplication
import it.valeriovaudi.vauthenticator.oauth2.clientapp.StoreClientApplication
import com.fasterxml.jackson.databind.ObjectMapper
import it.valeriovaudi.vauthenticator.cache.CacheOperation
import it.valeriovaudi.vauthenticator.cache.RedisCacheOperation
import it.valeriovaudi.vauthenticator.oauth2.clientapp.*
import it.valeriovaudi.vauthenticator.password.VAuthenticatorPasswordEncoder
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.security.crypto.password.PasswordEncoder
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
import java.time.Duration

@Configuration(proxyBeanMethods = false)
class ClientApplicationConfig {

@Bean
fun clientApplicationRepository(dynamoDbClient: DynamoDbClient,
passwordEncoder: PasswordEncoder,
@Value("\${vauthenticator.dynamo-db.client-application.table-name}") clientAppTableName: String) =
fun clientApplicationRepository(
dynamoDbClient: DynamoDbClient,
passwordEncoder: PasswordEncoder,
clientApplicationCacheOperation: CacheOperation<String, String>,
objectMapper: ObjectMapper,
@Value("\${vauthenticator.dynamo-db.client-application.table-name}") clientAppTableName: String
) =
CachedClientApplicationRepository(
ClientApplicationCacheContentConverter(objectMapper),
clientApplicationCacheOperation,
DynamoDbClientApplicationRepository(dynamoDbClient, clientAppTableName)
)

@Bean
fun readClientApplication(clientApplicationRepository: ClientApplicationRepository) =
ReadClientApplication(clientApplicationRepository)
ReadClientApplication(clientApplicationRepository)

@Bean
fun storeClientApplication(clientApplicationRepository: ClientApplicationRepository,
passwordEncoder: VAuthenticatorPasswordEncoder
fun storeClientApplication(
clientApplicationRepository: ClientApplicationRepository,
passwordEncoder: VAuthenticatorPasswordEncoder
) =
StoreClientApplication(clientApplicationRepository, passwordEncoder)

StoreClientApplication(clientApplicationRepository, passwordEncoder)

@Bean
fun clientApplicationCacheOperation(
redisTemplate: RedisTemplate<*, *>,
@Value("\${vauthenticator.dynamo-db.client-application.cache.ttl}") ttl: Duration,
@Value("\${vauthenticator.dynamo-db.client-application.cache.name}") cacheRegionName: String,
) = RedisCacheOperation<String, String>(
cacheName = cacheRegionName,
ttl = ttl,
redisTemplate = redisTemplate as RedisTemplate<String, String>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package it.valeriovaudi.vauthenticator.oauth2.clientapp

import it.valeriovaudi.vauthenticator.cache.CacheContentConverter
import it.valeriovaudi.vauthenticator.cache.CacheOperation
import java.util.*

class CachedClientApplicationRepository(
private val cacheContentConverter: CacheContentConverter<ClientApplication>,
private val cacheOperation: CacheOperation<String, String>,
private val delegate: ClientApplicationRepository
) : ClientApplicationRepository by delegate {

override fun findOne(clientAppId: ClientAppId): Optional<ClientApplication> {
return cacheOperation.get(clientAppId.content)
.flatMap { Optional.of(cacheContentConverter.getObjectFromCacheContentFor(it)) }
.or {
val clientApp = delegate.findOne(clientAppId)
clientApp.ifPresent {
val loadableContentIntoCache = cacheContentConverter.loadableContentIntoCacheFor(it)
cacheOperation.put(clientAppId.content, loadableContentIntoCache)
}
clientApp
}
}

override fun save(clientApp: ClientApplication) {
cacheOperation.evict(clientApp.clientAppId.content)
delegate.save(clientApp)
}

override fun delete(clientAppId: ClientAppId) {
cacheOperation.evict(clientAppId.content)
delegate.delete(clientAppId)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package it.valeriovaudi.vauthenticator.oauth2.clientapp

import java.util.*

data class ClientApplication(
val clientAppId: ClientAppId,
val secret: Secret,
Expand All @@ -11,7 +9,7 @@ data class ClientApplication(
val authorities: Authorities,
val accessTokenValidity: TokenTimeToLive,
val refreshTokenValidity: TokenTimeToLive,
val additionalInformation: Map<String, Objects> = emptyMap(),
val additionalInformation: Map<String, Any> = emptyMap(),
val autoApprove: AutoApprove = AutoApprove.approve,
val postLogoutRedirectUri: PostLogoutRedirectUri,
val logoutUri: LogoutUri,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package it.valeriovaudi.vauthenticator.oauth2.clientapp

import com.fasterxml.jackson.databind.ObjectMapper
import it.valeriovaudi.vauthenticator.cache.CacheContentConverter

class ClientApplicationCacheContentConverter(private val objectMapper: ObjectMapper) :
CacheContentConverter<ClientApplication> {
override fun getObjectFromCacheContentFor(cacheContent: String): ClientApplication =
objectMapper.readValue(cacheContent, Map::class.java)
.let {
ClientApplication(
clientAppId = ClientAppId(it["clientAppId"] as String),
secret = Secret(it["secret"] as String),
scopes = Scopes((it["scopes"] as List<String>).map { scope -> Scope(scope) }.toSet()),
authorizedGrantTypes =AuthorizedGrantTypes((it["authorizedGrantTypes"] as List<String>).map(AuthorizedGrantType::valueOf)),
webServerRedirectUri = CallbackUri(it["webServerRedirectUri"] as String),
authorities = Authorities((it["authorities"] as List<String>).map { authority -> Authority(authority) }),
accessTokenValidity = TokenTimeToLive(it["accessTokenValidity"].toString().toLong()),
refreshTokenValidity = TokenTimeToLive(it["refreshTokenValidity"].toString().toLong()),
additionalInformation = it["additionalInformation"] as Map<String, Any>,
autoApprove = AutoApprove(it["autoApprove"] as Boolean),
postLogoutRedirectUri = PostLogoutRedirectUri(it["postLogoutRedirectUri"] as String),
logoutUri = LogoutUri(it["logoutUri"] as String)
)

}


override fun loadableContentIntoCacheFor(source: ClientApplication): String =
objectMapper.writeValueAsString(
mapOf(
"clientAppId" to source.clientAppId.content,
"secret" to source.secret.content,
"scopes" to source.scopes.content.map(Scope::content),
"authorizedGrantTypes" to source.authorizedGrantTypes.content.map(AuthorizedGrantType::name),
"webServerRedirectUri" to source.webServerRedirectUri.content,
"authorities" to source.authorities.content.map(Authority::content),
"accessTokenValidity" to source.accessTokenValidity.content,
"refreshTokenValidity" to source.refreshTokenValidity.content,
"additionalInformation" to source.additionalInformation,
"autoApprove" to source.autoApprove.content,
"postLogoutRedirectUri" to source.postLogoutRedirectUri.content,
"logoutUri" to source.logoutUri.content
)
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,25 @@ package it.valeriovaudi.vauthenticator.account

import com.fasterxml.jackson.databind.ObjectMapper
import it.valeriovaudi.vauthenticator.account.AccountTestFixture.anAccount
import it.valeriovaudi.vauthenticator.support.JsonUtils.prettifyInOneLineJsonFrom
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.util.FileCopyUtils
import java.io.FileReader
import java.nio.file.Files
import java.nio.file.Paths

class AccountCacheContentConverterTest {
val underTest = AccountCacheContentConverter(ObjectMapper())

@Test
fun `when a complete account from cache is loaded`() {
val cacheContent = testUseCase("aCompleteAccount.json")
val actual = underTest.getObjectFromCacheContentFor(cacheContent)
val aCompleteAccountCacheContent = prettifyInOneLineJsonFrom(resourceTestFrom("aCompleteAccount.json"))
val actual = underTest.getObjectFromCacheContentFor(aCompleteAccountCacheContent)
val expected = aCompleteAccount()

Assertions.assertEquals(expected, actual)
}

@Test
fun `when a partial account from cache is loaded`() {
val cacheContent = testUseCase("aPartialAccount.json")
val cacheContent = prettifyInOneLineJsonFrom(resourceTestFrom("aPartialAccount.json"))
val actual = underTest.getObjectFromCacheContentFor(cacheContent)
val expected = aPartialAccount()

Expand All @@ -33,15 +30,15 @@ class AccountCacheContentConverterTest {
@Test
fun `when a complete account is made ready for the cache`() {
val actual = underTest.loadableContentIntoCacheFor(aCompleteAccount())
val expected = testUseCaseInASingleLine("aCompleteAccount.json")
val expected = prettifyInOneLineJsonFrom(resourceTestFrom("aCompleteAccount.json"))

Assertions.assertEquals(expected, actual)
}

@Test
fun `when a partial account is made ready for the cache`() {
val actual = underTest.loadableContentIntoCacheFor(aPartialAccount())
val expected = testUseCaseInASingleLine("aPartialAccount.json")
val expected = prettifyInOneLineJsonFrom(resourceTestFrom("aPartialAccount.json"))

Assertions.assertEquals(expected, actual)
}
Expand All @@ -54,10 +51,6 @@ class AccountCacheContentConverterTest {

private fun aPartialAccount() = anAccount()

private fun testUseCase(fileName: String) =
FileCopyUtils.copyToString(FileReader("src/test/resources/accounts/$fileName"))
private fun resourceTestFrom(fileName: String) = "accounts/$fileName"

private fun testUseCaseInASingleLine(fileName: String) =
Files.readAllLines(Paths.get("src/test/resources/accounts/$fileName"))
.joinToString("") { it.replace(": ", ":").trim() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package it.valeriovaudi.vauthenticator.document
import it.valeriovaudi.vauthenticator.support.DocumentUtils.documentBucket
import it.valeriovaudi.vauthenticator.support.DocumentUtils.initDocumentTests
import it.valeriovaudi.vauthenticator.support.DocumentUtils.s3Client
import it.valeriovaudi.vauthenticator.support.SecurityFixture.loadFileFor
import it.valeriovaudi.vauthenticator.support.FileUtils
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
Expand All @@ -18,7 +18,7 @@ internal class S3DocumentRepositoryTest {
val documentRepository = S3DocumentRepository(s3Client, documentBucket)
val document = documentRepository.loadDocument("mail", "templates/welcome.html")

val expected = loadFileFor("index.html")
val expected = FileUtils.loadFileFor("index.html")
assertEquals(expected, String(document))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package it.valeriovaudi.vauthenticator.oauth2.clientapp

import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import io.mockk.just
import io.mockk.runs
import io.mockk.verify
import it.valeriovaudi.vauthenticator.cache.CacheContentConverter
import it.valeriovaudi.vauthenticator.cache.CacheOperation
import it.valeriovaudi.vauthenticator.oauth2.clientapp.ClientAppFixture.aClientApp
import it.valeriovaudi.vauthenticator.oauth2.clientapp.ClientAppFixture.aClientAppId
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import java.util.*

@ExtendWith(MockKExtension::class)
class CachedClientApplicationRepositoryTest {

private val clientAppId = aClientAppId()
private val clientApplication = aClientApp(clientAppId)

@MockK
lateinit var delegate: ClientApplicationRepository

@MockK
lateinit var cacheOperation: CacheOperation<String, String>

@MockK
lateinit var cacheContentConverter: CacheContentConverter<ClientApplication>

lateinit var underTest: ClientApplicationRepository

@BeforeEach
fun setUp() {
underTest = CachedClientApplicationRepository(cacheContentConverter, cacheOperation, delegate)
}


@Test
internal fun `when a client app is not found into the cache then it is loaded from database and loaded into the cache`() {
every { cacheOperation.get(clientAppId.content) } returns Optional.empty()
every { delegate.findOne(clientAppId) } returns Optional.of(clientApplication)
every { cacheContentConverter.loadableContentIntoCacheFor(clientApplication) } returns "content"
every { cacheOperation.put(clientAppId.content, "content") } just runs

underTest.findOne(clientAppId)

verify { cacheOperation.get(clientAppId.content) }
verify { delegate.findOne(clientAppId) }
verify { cacheContentConverter.loadableContentIntoCacheFor(clientApplication) }
verify { cacheOperation.put(clientAppId.content, "content") }
}


@Test
internal fun `when a client application is found from the cache `() {
every { cacheOperation.get(clientAppId.content) } returns Optional.of("content")
every { cacheContentConverter.getObjectFromCacheContentFor("content") } returns clientApplication

underTest.findOne(clientAppId)

verify { cacheOperation.get(clientAppId.content) }
verify(exactly = 0) { delegate.findOne(clientAppId) }
verify { cacheContentConverter.getObjectFromCacheContentFor("content") }
verify(exactly = 0) { cacheOperation.put(clientAppId.content, "content") }
}

@Test
fun `when a client application is updated`() {
every { cacheOperation.evict(clientAppId.content) } just runs
every { delegate.save(clientApplication) } just runs

underTest.save(clientApplication)

verify { cacheOperation.evict(clientAppId.content) }
verify { delegate.save(clientApplication) }
}

@Test
fun `when a client application is deleted`() {
every { cacheOperation.evict(clientAppId.content) } just runs
every { delegate.delete(clientAppId) } just runs

underTest.delete(clientAppId)

verify { cacheOperation.evict(clientAppId.content) }
verify { delegate.delete(clientAppId) }
}


}
Loading

0 comments on commit ced7acb

Please sign in to comment.