Skip to content
This repository was archived by the owner on Jul 7, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ interface UploadImageUsecase {

data class Command(
val image: FileMetaData,
val userId: String
val userId: String? = null
)

data class Response(
val imageUrl: String
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@ import org.springframework.stereotype.Service
@Service
class ImageCommandService(
private val imageManagementPort: ImageManagementPort,
private val userManagementPort: UserManagementPort
private val userManagementPort: UserManagementPort,
) : UploadImageUsecase {
override fun upload(command: UploadImageUsecase.Command): UploadImageUsecase.Response {
val user = userManagementPort.getUserNotNull(DomainId(command.userId))
val user = command.userId?.let { userManagementPort.getUserNotNull(DomainId(it)) }

val uploadedImage = imageManagementPort.save(
ImageMetadata(
owner = user.id.value,
fileMetaData = command.image
val uploadedImage =
imageManagementPort.save(
ImageMetadata(
owner = user?.id?.value,
fileMetaData = command.image,
),
)
)
return UploadImageUsecase.Response(
imageUrl = uploadedImage.imageUrl
imageUrl = uploadedImage.imageUrl,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.asap.application.image.vo
import com.asap.common.file.FileMetaData

data class ImageMetadata(
val owner: String,
val fileMetaData: FileMetaData
) {
}
val owner: String?,
val fileMetaData: FileMetaData,
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import com.asap.application.image.vo.UploadedImage
import com.asap.application.user.port.out.UserManagementPort
import com.asap.common.file.FileMetaData
import com.asap.domain.UserFixture
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.nulls.shouldNotBeNull
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import java.io.InputStream

class ImageCommandServiceTest :
BehaviorSpec({
isolationMode = IsolationMode.InstancePerLeaf

val mockImageManagementPort = mockk<ImageManagementPort>(relaxed = true)
val mockUserManagementPort = mockk<UserManagementPort>(relaxed = true)
Expand Down Expand Up @@ -56,4 +59,36 @@ class ImageCommandServiceTest :
}
}
}

given("userId가 null인 이미지 업로드 요청이 들어올 때") {
val command =
UploadImageUsecase.Command(
userId = null,
image =
FileMetaData(
name = "name",
contentType = "contentType",
size = 1L,
inputStream = InputStream.nullInputStream(),
),
)
every {
mockImageManagementPort.save(any())
} returns
UploadedImage(
imageUrl = "imageUrl",
)
`when`("이미지 업로드 요청을 처리하면") {
val response = imageCommandService.upload(command)
then("getUserNotNull 메서드가 호출되지 않아야 한다") {
verify(exactly = 0) { mockUserManagementPort.getUserNotNull(any()) }
}
then("이미지가 저장되어야 한다") {
response.imageUrl shouldNotBeNull {
this.isNotBlank()
this.isNotEmpty()
}
}
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
class WebConfig(
private val accessUserArgumentResolver: AccessUserArgumentResolver
private val accessUserArgumentResolver: AccessUserArgumentResolver,
) : WebMvcConfigurer {

override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
registry
.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
Expand All @@ -21,4 +21,4 @@ class WebConfig(
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(accessUserArgumentResolver)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,17 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer

@Component
class AccessUserArgumentResolver: HandlerMethodArgumentResolver {

override fun supportsParameter(parameter: MethodParameter): Boolean {
return parameter.hasParameterAnnotation(AccessUser::class.java)
}
class AccessUserArgumentResolver : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean = parameter.hasParameterAnnotation(AccessUser::class.java)

override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?
binderFactory: WebDataBinderFactory?,
): Any? {
val userAuthentication = SecurityContextHolder.getContext().getAuthentication() as UserAuthentication
val userId = userAuthentication.getDetails()
return userId
val authentication = SecurityContextHolder.getContext()?.getAuthentication()
val userAuthentication = authentication as? UserAuthentication
return userAuthentication?.getDetails()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import org.springframework.web.multipart.MultipartFile
interface ImageApi {
@Operation(
summary = "이미지 업로드",
description = "이미지를 업로드합니다.",
description = "이미지를 업로드합니다. 회원과 비회원 모두 이용 가능합니다.",
)
@PostMapping(consumes = ["multipart/form-data"])
@ApiResponses(
Expand All @@ -28,8 +28,8 @@ interface ImageApi {
headers = [
Header(
name = "Authorization",
description = "액세스 토큰",
required = true,
description = "액세스 토큰 (선택사항)",
required = false,
),
],
),
Expand All @@ -41,6 +41,6 @@ interface ImageApi {
)
fun uploadImage(
@RequestPart image: MultipartFile,
@AccessUser userId: String,
@AccessUser userId: String?,
): UploadImageResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class ImageController(
) : ImageApi {
override fun uploadImage(
image: MultipartFile,
userId: String,
userId: String?,
): UploadImageResponse {
val response =
uploadImageUsecase.upload(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,39 @@ class ImageControllerTest : AcceptanceSupporter() {
}
}
}

@Test
fun uploadImageWithoutAuthentication() {
// given
val mockFile = MockMultipartFile("image", "test.jpg", "image/jpeg", "test".toByteArray())
val mockFileMetaData = FileMetaData("test.jpg", 4, "image/jpeg", mockFile.inputStream)
BDDMockito
.given(fileConverter.convert(mockFile))
.willReturn(mockFileMetaData)
BDDMockito
.given(
uploadImageUsecase.upload(
UploadImageUsecase.Command(
image = mockFileMetaData,
userId = null,
),
),
).willReturn(UploadImageUsecase.Response("imageUrl"))
// when
val response =
mockMvc.multipart("/api/v1/images") {
file(mockFile)
contentType = MediaType.MULTIPART_FORM_DATA
// No Authorization header
}
// then
response.andExpect {
status { isOk() }
jsonPath("$.imageUrl") {
exists()
isString()
isNotEmpty()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ class SecurityContextHolder {
companion object {
private val contextHolder = ThreadLocal<SecurityContext<*, *>>()

fun getContext(): SecurityContext<*, *> {
return contextHolder.get()
}
fun getContext(): SecurityContext<*, *>? = contextHolder.get()

fun setContext(context: SecurityContext<*, *>) {
contextHolder.set(context)
Expand All @@ -16,4 +14,4 @@ class SecurityContextHolder {
contextHolder.remove()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class S3ImageManagementAdapter(
private val s3Template: S3Template,
) : ImageManagementPort {
override fun save(image: ImageMetadata): UploadedImage {
val key = "${image.owner}/${UUID.randomUUID()}"
val owner = image.owner ?: ANONYMOUS_OWNER_ID

val key = "$owner/${UUID.randomUUID()}"

val resource =
s3Template.upload(
Expand All @@ -33,5 +35,6 @@ class S3ImageManagementAdapter(

companion object {
private const val BUCKET_NAME = "lettering-images"
private const val ANONYMOUS_OWNER_ID = "anonymous"
}
}