1+ package com .Rootin .domain .s3 .controller ;
2+
3+ import com .Rootin .domain .garden .service .PotService ;
4+ import com .Rootin .domain .s3 .dto .PresignedUrlRequest ;
5+ import com .Rootin .global .config .TestSecurityConfig ;
6+ import com .Rootin .global .exception .CustomException ;
7+ import com .Rootin .global .jwt .JwtUserDetails ;
8+ import com .Rootin .global .s3 .S3Service ;
9+ import com .fasterxml .jackson .databind .ObjectMapper ;
10+ import org .junit .jupiter .api .BeforeEach ;
11+ import org .junit .jupiter .api .DisplayName ;
12+ import org .junit .jupiter .api .Test ;
13+ import org .junit .jupiter .params .ParameterizedTest ;
14+ import org .junit .jupiter .params .provider .ValueSource ;
15+ import org .springframework .beans .factory .annotation .Autowired ;
16+ import org .springframework .boot .test .autoconfigure .web .servlet .WebMvcTest ;
17+ import org .springframework .boot .test .mock .mockito .MockBean ;
18+ import org .springframework .context .annotation .Import ;
19+ import org .springframework .http .MediaType ;
20+ import org .springframework .security .core .authority .SimpleGrantedAuthority ;
21+ import org .springframework .test .context .ActiveProfiles ;
22+ import org .springframework .test .web .servlet .MockMvc ;
23+
24+ import java .util .List ;
25+
26+ import static org .hamcrest .Matchers .containsString ;
27+ import static org .mockito .ArgumentMatchers .anyString ;
28+ import static org .mockito .ArgumentMatchers .eq ;
29+ import static org .mockito .BDDMockito .doNothing ;
30+ import static org .mockito .BDDMockito .doThrow ;
31+ import static org .mockito .BDDMockito .given ;
32+ import static org .springframework .security .test .web .servlet .request .SecurityMockMvcRequestPostProcessors .user ;
33+ import static org .springframework .test .web .servlet .request .MockMvcRequestBuilders .post ;
34+ import static org .springframework .test .web .servlet .result .MockMvcResultMatchers .jsonPath ;
35+ import static org .springframework .test .web .servlet .result .MockMvcResultMatchers .status ;
36+
37+ @ WebMvcTest (PresignedUrlController .class )
38+ @ ActiveProfiles ("test" )
39+ @ Import (TestSecurityConfig .class )
40+ class PresignedUrlControllerTest {
41+
42+ @ Autowired
43+ private MockMvc mockMvc ;
44+
45+ @ Autowired
46+ private ObjectMapper objectMapper ;
47+
48+ @ MockBean
49+ private S3Service s3Service ;
50+
51+ @ MockBean
52+ private PotService potService ;
53+
54+ private JwtUserDetails userDetails ;
55+
56+ private static final Long USER_ID = 1L ;
57+ private static final Long POT_ID = 10L ;
58+ private static final String PRESIGNED_URL = "https://rootin-bucket.s3.ap-northeast-2.amazonaws.com/til-images/1/10/uuid.jpg?X-Amz-Signature=abc" ;
59+ private static final String IMAGE_URL = "https://rootin-bucket.s3.ap-northeast-2.amazonaws.com/til-images/1/10/uuid.jpg" ;
60+
61+ @ BeforeEach
62+ void setUp () {
63+ userDetails = new JwtUserDetails (
64+ USER_ID ,
65+ "test@rootin.com" ,
66+ List .of (new SimpleGrantedAuthority ("ROLE_USER" ))
67+ );
68+ }
69+
70+ @ Test
71+ @ DisplayName ("유효한 JPEG 요청이면 presignedUrl과 imageUrl을 포함한 200 OK를 반환한다" )
72+ void getPresignedUrl_jpeg_success () throws Exception {
73+ PresignedUrlRequest request = buildRequest ("image/jpeg" , POT_ID );
74+
75+ doNothing ().when (potService ).validateOwnership (USER_ID , POT_ID );
76+ given (s3Service .generatePresignedPutUrl (anyString (), eq ("image/jpeg" ))).willReturn (PRESIGNED_URL );
77+ given (s3Service .getFileUrl (anyString ())).willReturn (IMAGE_URL );
78+
79+ mockMvc .perform (post ("/tils/image/presigned-url" )
80+ .with (user (userDetails ))
81+ .contentType (MediaType .APPLICATION_JSON )
82+ .content (objectMapper .writeValueAsString (request )))
83+ .andExpect (status ().isOk ())
84+ .andExpect (jsonPath ("$.success" ).value (true ))
85+ .andExpect (jsonPath ("$.data.presignedUrl" ).value (containsString ("X-Amz-Signature" )))
86+ .andExpect (jsonPath ("$.data.imageUrl" ).value (containsString ("til-images" )));
87+ }
88+
89+ @ ParameterizedTest
90+ @ ValueSource (strings = {"image/png" , "image/webp" })
91+ @ DisplayName ("image/png, image/webp contentType도 정상 처리되어 200 OK를 반환한다" )
92+ void getPresignedUrl_supportedContentTypes_success (String contentType ) throws Exception {
93+ PresignedUrlRequest request = buildRequest (contentType , POT_ID );
94+
95+ doNothing ().when (potService ).validateOwnership (USER_ID , POT_ID );
96+ given (s3Service .generatePresignedPutUrl (anyString (), eq (contentType ))).willReturn (PRESIGNED_URL );
97+ given (s3Service .getFileUrl (anyString ())).willReturn (IMAGE_URL );
98+
99+ mockMvc .perform (post ("/tils/image/presigned-url" )
100+ .with (user (userDetails ))
101+ .contentType (MediaType .APPLICATION_JSON )
102+ .content (objectMapper .writeValueAsString (request )))
103+ .andExpect (status ().isOk ())
104+ .andExpect (jsonPath ("$.success" ).value (true ));
105+ }
106+
107+ @ Test
108+ @ DisplayName ("contentType이 blank이면 @NotBlank 검증에 실패해 400 Bad Request를 반환한다" )
109+ void getPresignedUrl_blankContentType_returns400 () throws Exception {
110+ PresignedUrlRequest request = buildRequest ("" , POT_ID );
111+
112+ mockMvc .perform (post ("/tils/image/presigned-url" )
113+ .with (user (userDetails ))
114+ .contentType (MediaType .APPLICATION_JSON )
115+ .content (objectMapper .writeValueAsString (request )))
116+ .andExpect (status ().isBadRequest ());
117+ }
118+
119+ @ Test
120+ @ DisplayName ("potId가 null이면 @NotNull 검증에 실패해 400 Bad Request를 반환한다" )
121+ void getPresignedUrl_nullPotId_returns400 () throws Exception {
122+ String requestJson = """
123+ { "contentType": "image/jpeg" }
124+ """ ;
125+
126+ mockMvc .perform (post ("/tils/image/presigned-url" )
127+ .with (user (userDetails ))
128+ .contentType (MediaType .APPLICATION_JSON )
129+ .content (requestJson ))
130+ .andExpect (status ().isBadRequest ());
131+ }
132+
133+ @ Test
134+ @ DisplayName ("지원하지 않는 contentType(image/gif)이면 400 Bad Request를 반환한다" )
135+ void getPresignedUrl_unsupportedContentType_returns400 () throws Exception {
136+ PresignedUrlRequest request = buildRequest ("image/gif" , POT_ID );
137+
138+ doNothing ().when (potService ).validateOwnership (USER_ID , POT_ID );
139+
140+ mockMvc .perform (post ("/tils/image/presigned-url" )
141+ .with (user (userDetails ))
142+ .contentType (MediaType .APPLICATION_JSON )
143+ .content (objectMapper .writeValueAsString (request )))
144+ .andExpect (status ().isBadRequest ());
145+ }
146+
147+ @ Test
148+ @ DisplayName ("화분 소유자가 아닌 경우 403 Forbidden을 반환한다" )
149+ void getPresignedUrl_notOwner_returns403 () throws Exception {
150+ PresignedUrlRequest request = buildRequest ("image/jpeg" , POT_ID );
151+
152+ doThrow (CustomException .forbidden ("해당 화분에 접근할 권한이 없습니다." ))
153+ .when (potService ).validateOwnership (USER_ID , POT_ID );
154+
155+ mockMvc .perform (post ("/tils/image/presigned-url" )
156+ .with (user (userDetails ))
157+ .contentType (MediaType .APPLICATION_JSON )
158+ .content (objectMapper .writeValueAsString (request )))
159+ .andExpect (status ().isForbidden ());
160+ }
161+
162+ @ Test
163+ @ DisplayName ("존재하지 않는 화분 ID이면 404 Not Found를 반환한다" )
164+ void getPresignedUrl_potNotFound_returns404 () throws Exception {
165+ Long unknownPotId = 999L ;
166+ PresignedUrlRequest request = buildRequest ("image/jpeg" , unknownPotId );
167+
168+ doThrow (CustomException .notFound ("존재하지 않는 화분입니다. ID: " + unknownPotId ))
169+ .when (potService ).validateOwnership (USER_ID , unknownPotId );
170+
171+ mockMvc .perform (post ("/tils/image/presigned-url" )
172+ .with (user (userDetails ))
173+ .contentType (MediaType .APPLICATION_JSON )
174+ .content (objectMapper .writeValueAsString (request )))
175+ .andExpect (status ().isNotFound ());
176+ }
177+
178+ @ Test
179+ @ DisplayName ("요청 바디가 없으면 400 Bad Request를 반환한다" )
180+ void getPresignedUrl_emptyBody_returns400 () throws Exception {
181+ mockMvc .perform (post ("/tils/image/presigned-url" )
182+ .with (user (userDetails ))
183+ .contentType (MediaType .APPLICATION_JSON )
184+ .content ("{}" ))
185+ .andExpect (status ().isBadRequest ());
186+ }
187+
188+ private PresignedUrlRequest buildRequest (String contentType , Long potId ) {
189+ PresignedUrlRequest request = new PresignedUrlRequest ();
190+ request .setContentType (contentType );
191+ request .setPotId (potId );
192+ return request ;
193+ }
194+ }
0 commit comments