Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
… into feat/YS-31
  • Loading branch information
chock-cho committed Jan 2, 2025
2 parents 91ad19d + c88d379 commit fe4ba57
Show file tree
Hide file tree
Showing 15 changed files with 243 additions and 9 deletions.
5 changes: 3 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("io.github.cdimascio:java-dotenv:5.2.2")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
implementation ("io.awspring.cloud:spring-cloud-starter-aws:2.4.4")
compileOnly("org.projectlombok:lombok")
runtimeOnly("com.mysql:mysql-connector-j")
runtimeOnly("com.h2database:h2")
Expand Down Expand Up @@ -97,8 +98,8 @@ sonar {
property("sonar.sources", "src/main/kotlin")
property("sonar.sourceEncoding", "UTF-8")
property("sonar.exclusions", "**/test/**, **/resources/**, **/*Application*.kt, **/*Controller*.kt, " +
"**/*Config.kt, **/*Repository*.kt, **/*Dto*.kt, **/*Response*.kt, **/*Request*.kt, **/*Exception*.kt," +
"**/*Filter.kt, **/*Handler.kt, **/*Properties.kt, **/*Utils.kt")
"**/*Config.kt, **/*Entity*.kt, **/*Repository*.kt, **/*Dto*.kt, **/*Response*.kt, **/*Request*.kt, **/*Exception*.kt," +
"**/config/**, **/domain/gateway/**, **/domain/model/**, **/infrastructure/**, **/presentation/**, **/util/**")
property("sonar.test.inclusions", "**/*Test.kt")
property("sonar.kotlin.coveragePlugin", "jacoco")
}
Expand Down
2 changes: 0 additions & 2 deletions src/main/kotlin/com/dobby/backend/DobbyBackendApplication.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.dobby.backend

import com.dobby.backend.infrastructure.config.TokenProperties
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
import io.github.cdimascio.dotenv.Dotenv;

Expand Down
5 changes: 3 additions & 2 deletions src/main/kotlin/com/dobby/backend/config/ApplicationConfig.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.dobby.backend.config

import com.dobby.backend.infrastructure.config.TokenProperties
import com.dobby.backend.infrastructure.config.properties.S3Properties
import com.dobby.backend.infrastructure.config.properties.TokenProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration

@Configuration
@EnableConfigurationProperties(TokenProperties::class)
@EnableConfigurationProperties(TokenProperties::class, S3Properties::class)
class ApplicationConfig {
}
7 changes: 7 additions & 0 deletions src/main/kotlin/com/dobby/backend/domain/gateway/S3Gateway.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.dobby.backend.domain.gateway

import com.dobby.backend.presentation.api.dto.response.PreSignedUrlResponse

interface S3Gateway {
fun getPreSignedUrl(fileName: String): PreSignedUrlResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.dobby.backend.infrastructure.config

import com.amazonaws.auth.AWSStaticCredentialsProvider
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.services.s3.AmazonS3
import com.amazonaws.services.s3.AmazonS3ClientBuilder
import com.dobby.backend.infrastructure.config.properties.S3Properties
import lombok.RequiredArgsConstructor
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
@EnableConfigurationProperties(S3Properties::class)
@RequiredArgsConstructor
class S3Config {

private val properties: S3Properties

@Autowired
constructor(properties: S3Properties) {
this.properties = properties
}

@Bean
fun amazonS3Client(): AmazonS3? {
val awsCredentialsProvider = BasicAWSCredentials(properties.credentials.accessKey, properties.credentials.secretKey)

return AmazonS3ClientBuilder.standard()
.withRegion(properties.region.static)
.withCredentials(AWSStaticCredentialsProvider(awsCredentialsProvider))
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.dobby.backend.infrastructure.config.properties

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "cloud.aws")
data class S3Properties(
val s3: S3,
val region: Region,
val credentials: Credentials
) {
data class S3(
val bucket: String
)

data class Region(
val static: String
)

data class Credentials(
val accessKey: String,
val secretKey: String
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.dobby.backend.infrastructure.config
package com.dobby.backend.infrastructure.config.properties

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dobby.backend.infrastructure.gateway

import com.dobby.backend.domain.gateway.S3Gateway
import com.dobby.backend.infrastructure.s3.S3PreSignedUrlProvider
import com.dobby.backend.presentation.api.dto.response.PreSignedUrlResponse
import org.springframework.stereotype.Component

@Component
class S3GatewayImpl(
private val s3PreSignedUrlProvider: S3PreSignedUrlProvider
) : S3Gateway {

override fun getPreSignedUrl(fileName: String): PreSignedUrlResponse {
return s3PreSignedUrlProvider.getPreSignedUrl(fileName)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.dobby.backend.infrastructure.s3

import com.amazonaws.AmazonServiceException
import com.amazonaws.HttpMethod
import com.amazonaws.services.s3.AmazonS3
import com.amazonaws.services.s3.Headers
import com.amazonaws.services.s3.model.CannedAccessControlList
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest
import com.dobby.backend.domain.exception.InvalidInputException
import com.dobby.backend.presentation.api.dto.response.PreSignedUrlResponse
import com.dobby.backend.util.generateULID
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.util.*

@Component
class S3PreSignedUrlProvider(
private val amazonS3Client: AmazonS3,
) {

@Value("\${cloud.aws.s3.bucket}")
lateinit var bucket: String

fun getPreSignedUrl(fileName: String): PreSignedUrlResponse {
val generatePreSignedUrlRequest = getGenerateImagePreSignedUrlRequest(fileName)
return PreSignedUrlResponse(generatePreSignedUrl(generatePreSignedUrlRequest))
}

private fun generatePreSignedUrl(generatePresignedUrlRequest: GeneratePresignedUrlRequest): String {
return try {
amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest).toString()
} catch (e: AmazonServiceException) {
throw IllegalStateException("Pre-signed Url 생성 실패했습니다.")
}
}

private fun getGenerateImagePreSignedUrlRequest(fileName: String): GeneratePresignedUrlRequest {
val savedImageName = generateUniqueImageName(fileName)
val savedImagePath = "images/$savedImageName"
return getPreSignedUrlRequest(bucket, savedImagePath)
}

private fun getPreSignedUrlRequest(bucket: String, fileName: String): GeneratePresignedUrlRequest {
return GeneratePresignedUrlRequest(bucket, fileName)
.withMethod(HttpMethod.PUT)
.withExpiration(Date(System.currentTimeMillis() + 180000))
.apply {
addRequestParameter(Headers.S3_CANNED_ACL, CannedAccessControlList.PublicRead.toString())
}
}

private fun generateUniqueImageName(fileName: String): String {
val lastDotIndex = fileName.lastIndexOf(".")
if (lastDotIndex == -1 || lastDotIndex == fileName.length - 1) {
throw InvalidInputException()
}
val ext = fileName.substring(lastDotIndex)
return "${generateULID()}$ext"
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.dobby.backend.infrastructure.token

import com.dobby.backend.domain.exception.*
import com.dobby.backend.infrastructure.config.TokenProperties
import com.dobby.backend.infrastructure.config.properties.TokenProperties
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.JwtException
import io.jsonwebtoken.Jwts
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.stereotype.Component
Expand All @@ -13,6 +14,7 @@ import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec

@Component
@EnableConfigurationProperties(TokenProperties::class)
class JwtTokenProvider(
private val tokenProperties: TokenProperties
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.dobby.backend.presentation.api.dto.response

data class PreSignedUrlResponse(
val url: String
)
12 changes: 11 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,14 @@ app:
secretKey: ${JWT_SECRET_KEY}
expiration:
access: ${JWT_ACCESS_EXPIRATION}
refresh: ${JWT_REFRESH_EXPIRATION}
refresh: ${JWT_REFRESH_EXPIRATION}

cloud:
aws:
s3:
bucket: ${S3_BUCKET_NAME}
region:
static: ${S3_REGION}
credentials:
access-key: ${S3_ACCESS_KEY}
secret-key: ${S3_SECRET_KEY}
10 changes: 10 additions & 0 deletions src/main/resources/template-application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,13 @@ app:
expiration:
access: ${JWT_ACCESS_EXPIRATION}
refresh: ${JWT_REFRESH_EXPIRATION}

cloud:
aws:
s3:
bucket: ${S3_BUCKET_NAME}
region:
static: ${S3_REGION}
credentials:
access-key: ${S3_ACCESS_KEY}
secret-key: ${S3_SECRET_KEY}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.dobby.backend.infrastructure.s3

import com.amazonaws.services.s3.AmazonS3
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest
import com.dobby.backend.domain.exception.InvalidInputException
import com.dobby.backend.util.generateULID
import io.kotest.core.spec.style.BehaviorSpec
import org.junit.jupiter.api.Assertions.assertTrue
import org.mockito.Mockito.*
import org.springframework.boot.test.context.SpringBootTest
import java.net.URL
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull

@SpringBootTest
class S3PreSignedUrlProviderTest : BehaviorSpec({

val amazonS3Client = mock(AmazonS3::class.java)
val provider = S3PreSignedUrlProvider(amazonS3Client)
provider.bucket = "test-bucket"

given("이미지 파일 이름이 주어지고") {
val imageName = "test_image.jpg"
val mockPresignedUrl = mock(URL::class.java)

`when`("S3에서 PreSigned URL을 생성하면") {
`when`(amazonS3Client.generatePresignedUrl(any(GeneratePresignedUrlRequest::class.java))).thenReturn(mockPresignedUrl)

then("PreSignedUrlResponse가 반환된다") {
val response = provider.getPreSignedUrl(imageName)
assertNotNull(response)
assertEquals(mockPresignedUrl.toString(), response.url)
}
}
}

given("파일 이름에 확장자가 없는 경우") {
val imageName = "test_image" // 확장자가 없는 파일명

then("InvalidInputException이 발생한다") {
assertFailsWith<InvalidInputException> {
provider.getPreSignedUrl(imageName)
}
}
}

given("generateULID 함수가 호출되면") {
val ulid = generateULID()

then("ULID가 생성된다") {
assertNotNull(ulid)
assertTrue(ulid.isNotEmpty())
}
}
})
10 changes: 10 additions & 0 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,13 @@ app:
expiration:
access: 86400
refresh: 604800

cloud:
aws:
s3:
bucket: test
region:
static: test
credentials:
access-key: test
secret-key: test

0 comments on commit fe4ba57

Please sign in to comment.