Skip to content
This repository was archived by the owner on Jul 7, 2025. It is now read-only.

Commit c6f9d5e

Browse files
authored
Merge pull request #131 from ASAP-Lettering/ASAP-456
ASAP-456 비회원 이미지 업로드 로직 추가 및 테스트 보완
2 parents 7f895d3 + a953035 commit c6f9d5e

File tree

11 files changed

+107
-39
lines changed

11 files changed

+107
-39
lines changed

Application-Module/src/main/kotlin/com/asap/application/image/port/in/UploadImageUsecase.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ interface UploadImageUsecase {
99

1010
data class Command(
1111
val image: FileMetaData,
12-
val userId: String
12+
val userId: String? = null
1313
)
1414

1515
data class Response(
1616
val imageUrl: String
1717
)
18-
}
18+
}

Application-Module/src/main/kotlin/com/asap/application/image/service/ImageCommandService.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,20 @@ import org.springframework.stereotype.Service
1010
@Service
1111
class ImageCommandService(
1212
private val imageManagementPort: ImageManagementPort,
13-
private val userManagementPort: UserManagementPort
13+
private val userManagementPort: UserManagementPort,
1414
) : UploadImageUsecase {
1515
override fun upload(command: UploadImageUsecase.Command): UploadImageUsecase.Response {
16-
val user = userManagementPort.getUserNotNull(DomainId(command.userId))
16+
val user = command.userId?.let { userManagementPort.getUserNotNull(DomainId(it)) }
1717

18-
val uploadedImage = imageManagementPort.save(
19-
ImageMetadata(
20-
owner = user.id.value,
21-
fileMetaData = command.image
18+
val uploadedImage =
19+
imageManagementPort.save(
20+
ImageMetadata(
21+
owner = user?.id?.value,
22+
fileMetaData = command.image,
23+
),
2224
)
23-
)
2425
return UploadImageUsecase.Response(
25-
imageUrl = uploadedImage.imageUrl
26+
imageUrl = uploadedImage.imageUrl,
2627
)
2728
}
28-
}
29+
}

Application-Module/src/main/kotlin/com/asap/application/image/vo/ImageMetadata.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.asap.application.image.vo
33
import com.asap.common.file.FileMetaData
44

55
data class ImageMetadata(
6-
val owner: String,
7-
val fileMetaData: FileMetaData
8-
) {
9-
}
6+
val owner: String?,
7+
val fileMetaData: FileMetaData,
8+
)

Application-Module/src/test/kotlin/com/asap/application/image/service/ImageCommandServiceTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ import com.asap.application.image.vo.UploadedImage
66
import com.asap.application.user.port.out.UserManagementPort
77
import com.asap.common.file.FileMetaData
88
import com.asap.domain.UserFixture
9+
import io.kotest.core.spec.IsolationMode
910
import io.kotest.core.spec.style.BehaviorSpec
1011
import io.kotest.matchers.nulls.shouldNotBeNull
1112
import io.mockk.every
1213
import io.mockk.mockk
14+
import io.mockk.verify
1315
import java.io.InputStream
1416

1517
class ImageCommandServiceTest :
1618
BehaviorSpec({
19+
isolationMode = IsolationMode.InstancePerLeaf
1720

1821
val mockImageManagementPort = mockk<ImageManagementPort>(relaxed = true)
1922
val mockUserManagementPort = mockk<UserManagementPort>(relaxed = true)
@@ -56,4 +59,36 @@ class ImageCommandServiceTest :
5659
}
5760
}
5861
}
62+
63+
given("userId가 null인 이미지 업로드 요청이 들어올 때") {
64+
val command =
65+
UploadImageUsecase.Command(
66+
userId = null,
67+
image =
68+
FileMetaData(
69+
name = "name",
70+
contentType = "contentType",
71+
size = 1L,
72+
inputStream = InputStream.nullInputStream(),
73+
),
74+
)
75+
every {
76+
mockImageManagementPort.save(any())
77+
} returns
78+
UploadedImage(
79+
imageUrl = "imageUrl",
80+
)
81+
`when`("이미지 업로드 요청을 처리하면") {
82+
val response = imageCommandService.upload(command)
83+
then("getUserNotNull 메서드가 호출되지 않아야 한다") {
84+
verify(exactly = 0) { mockUserManagementPort.getUserNotNull(any()) }
85+
}
86+
then("이미지가 저장되어야 한다") {
87+
response.imageUrl shouldNotBeNull {
88+
this.isNotBlank()
89+
this.isNotEmpty()
90+
}
91+
}
92+
}
93+
}
5994
})

Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/config/WebConfig.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
88

99
@Configuration
1010
class WebConfig(
11-
private val accessUserArgumentResolver: AccessUserArgumentResolver
11+
private val accessUserArgumentResolver: AccessUserArgumentResolver,
1212
) : WebMvcConfigurer {
13-
1413
override fun addCorsMappings(registry: CorsRegistry) {
15-
registry.addMapping("/**")
14+
registry
15+
.addMapping("/**")
1616
.allowedOrigins("*")
1717
.allowedMethods("GET", "POST", "PUT", "DELETE")
1818
.allowedHeaders("*")
@@ -21,4 +21,4 @@ class WebConfig(
2121
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
2222
resolvers.add(accessUserArgumentResolver)
2323
}
24-
}
24+
}

Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/security/annotation/AccessUserArgumentResolver.kt

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,17 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver
1010
import org.springframework.web.method.support.ModelAndViewContainer
1111

1212
@Component
13-
class AccessUserArgumentResolver: HandlerMethodArgumentResolver {
14-
15-
override fun supportsParameter(parameter: MethodParameter): Boolean {
16-
return parameter.hasParameterAnnotation(AccessUser::class.java)
17-
}
13+
class AccessUserArgumentResolver : HandlerMethodArgumentResolver {
14+
override fun supportsParameter(parameter: MethodParameter): Boolean = parameter.hasParameterAnnotation(AccessUser::class.java)
1815

1916
override fun resolveArgument(
2017
parameter: MethodParameter,
2118
mavContainer: ModelAndViewContainer?,
2219
webRequest: NativeWebRequest,
23-
binderFactory: WebDataBinderFactory?
20+
binderFactory: WebDataBinderFactory?,
2421
): Any? {
25-
val userAuthentication = SecurityContextHolder.getContext().getAuthentication() as UserAuthentication
26-
val userId = userAuthentication.getDetails()
27-
return userId
22+
val authentication = SecurityContextHolder.getContext()?.getAuthentication()
23+
val userAuthentication = authentication as? UserAuthentication
24+
return userAuthentication?.getDetails()
2825
}
29-
}
26+
}

Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/image/api/ImageApi.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import org.springframework.web.multipart.MultipartFile
1717
interface ImageApi {
1818
@Operation(
1919
summary = "이미지 업로드",
20-
description = "이미지를 업로드합니다.",
20+
description = "이미지를 업로드합니다. 회원과 비회원 모두 이용 가능합니다.",
2121
)
2222
@PostMapping(consumes = ["multipart/form-data"])
2323
@ApiResponses(
@@ -28,8 +28,8 @@ interface ImageApi {
2828
headers = [
2929
Header(
3030
name = "Authorization",
31-
description = "액세스 토큰",
32-
required = true,
31+
description = "액세스 토큰 (선택사항)",
32+
required = false,
3333
),
3434
],
3535
),
@@ -41,6 +41,6 @@ interface ImageApi {
4141
)
4242
fun uploadImage(
4343
@RequestPart image: MultipartFile,
44-
@AccessUser userId: String,
44+
@AccessUser userId: String?,
4545
): UploadImageResponse
4646
}

Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/image/controller/ImageController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class ImageController(
1414
) : ImageApi {
1515
override fun uploadImage(
1616
image: MultipartFile,
17-
userId: String,
17+
userId: String?,
1818
): UploadImageResponse {
1919
val response =
2020
uploadImageUsecase.upload(

Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/image/controller/ImageControllerTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,39 @@ class ImageControllerTest : AcceptanceSupporter() {
5454
}
5555
}
5656
}
57+
58+
@Test
59+
fun uploadImageWithoutAuthentication() {
60+
// given
61+
val mockFile = MockMultipartFile("image", "test.jpg", "image/jpeg", "test".toByteArray())
62+
val mockFileMetaData = FileMetaData("test.jpg", 4, "image/jpeg", mockFile.inputStream)
63+
BDDMockito
64+
.given(fileConverter.convert(mockFile))
65+
.willReturn(mockFileMetaData)
66+
BDDMockito
67+
.given(
68+
uploadImageUsecase.upload(
69+
UploadImageUsecase.Command(
70+
image = mockFileMetaData,
71+
userId = null,
72+
),
73+
),
74+
).willReturn(UploadImageUsecase.Response("imageUrl"))
75+
// when
76+
val response =
77+
mockMvc.multipart("/api/v1/images") {
78+
file(mockFile)
79+
contentType = MediaType.MULTIPART_FORM_DATA
80+
// No Authorization header
81+
}
82+
// then
83+
response.andExpect {
84+
status { isOk() }
85+
jsonPath("$.imageUrl") {
86+
exists()
87+
isString()
88+
isNotEmpty()
89+
}
90+
}
91+
}
5792
}

Common-Module/src/main/kotlin/com/asap/common/security/SecurityContextHolder.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ class SecurityContextHolder {
44
companion object {
55
private val contextHolder = ThreadLocal<SecurityContext<*, *>>()
66

7-
fun getContext(): SecurityContext<*, *> {
8-
return contextHolder.get()
9-
}
7+
fun getContext(): SecurityContext<*, *>? = contextHolder.get()
108

119
fun setContext(context: SecurityContext<*, *>) {
1210
contextHolder.set(context)
@@ -16,4 +14,4 @@ class SecurityContextHolder {
1614
contextHolder.remove()
1715
}
1816
}
19-
}
17+
}

0 commit comments

Comments
 (0)