diff --git a/backend/emm-sale/src/documentTest/asciidoc/index.adoc b/backend/emm-sale/src/documentTest/asciidoc/index.adoc index acf07b463..c78058d74 100644 --- a/backend/emm-sale/src/documentTest/asciidoc/index.adoc +++ b/backend/emm-sale/src/documentTest/asciidoc/index.adoc @@ -277,13 +277,10 @@ include::{snippets}/find-recruitment-post/response-fields.adoc[] POST -[source] ----- -/events?name=인프콘 2023&location=코엑스&informationUrl=https://~~~&startDateTime=2023:06:01:12:00:00&endDateTime=2023:09:01:12:00:00&applyStartDateTime=2023:05:01:12:00:00&applyEndDateTime=2023:06:01:12:00:00&tags=백엔드,안드로이드&imageUrl=https://image.url&type=CONFERENCE&eventMode=ON_OFFLINE&paymentType=FREE ----- +.HTTP request +include::{snippets}/add-event/http-request.adoc[] .HTTP request 설명 -include::{snippets}/add-event/request-parameters.adoc[] include::{snippets}/add-event/request-parts.adoc[] .HTTP response @@ -298,7 +295,7 @@ include::{snippets}/add-event/response-fields.adoc[] include::{snippets}/update-event/http-request.adoc[] .HTTP request 설명 -include::{snippets}/update-event/request-fields.adoc[] +include::{snippets}/update-event/request-parts.adoc[] .HTTP response include::{snippets}/update-event/http-response.adoc[] diff --git a/backend/emm-sale/src/documentTest/java/com/emmsale/EventApiTest.java b/backend/emm-sale/src/documentTest/java/com/emmsale/EventApiTest.java index b1a4de421..5d919b0ce 100644 --- a/backend/emm-sale/src/documentTest/java/com/emmsale/EventApiTest.java +++ b/backend/emm-sale/src/documentTest/java/com/emmsale/EventApiTest.java @@ -1,17 +1,14 @@ package com.emmsale; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.emmsale.event.EventFixture; @@ -26,11 +23,12 @@ import com.emmsale.event.domain.PaymentType; import com.emmsale.tag.TagFixture; import com.emmsale.tag.application.dto.TagRequest; +import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; @@ -43,12 +41,12 @@ import org.mockito.ArgumentMatchers; import org.mockito.Mockito; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.PayloadDocumentation; -import org.springframework.restdocs.payload.RequestFieldsSnippet; import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.restdocs.request.RequestDocumentation; import org.springframework.restdocs.request.RequestParametersSnippet; @@ -176,9 +174,9 @@ void findEvents() throws Exception { ); Mockito.when(eventService.findEvents(any(EventType.class), - any(LocalDate.class), ArgumentMatchers.eq("2023-07-01"), - ArgumentMatchers.eq("2023-07-31"), - ArgumentMatchers.eq(null), any())).thenReturn(eventResponses); + any(LocalDate.class), eq("2023-07-01"), + eq("2023-07-31"), + eq(null), any())).thenReturn(eventResponses); // when & then mockMvc.perform(get("/events") @@ -195,53 +193,81 @@ void findEvents() throws Exception { @DisplayName("이벤트를 성공적으로 업데이트하면 200, OK를 반환한다.") void updateEventTest() throws Exception { //given + final MockMultipartFile image1 = new MockMultipartFile( + "picture", + "picture.jpg", + MediaType.TEXT_PLAIN_VALUE, + "test data".getBytes() + ); + + final MockMultipartFile image2 = new MockMultipartFile( + "picture", + "picture.jpg", + MediaType.TEXT_PLAIN_VALUE, + "test data".getBytes() + ); final long eventId = 1L; final Event event = EventFixture.인프콘_2023(); final List tags = Stream.of(TagFixture.백엔드(), TagFixture.안드로이드()) .map(tag -> new TagRequest(tag.getName())).collect(Collectors.toList()); - final EventDetailRequest request = new EventDetailRequest(event.getName(), event.getLocation(), - event.getInformationUrl(), event.getEventPeriod().getStartDate(), + final EventDetailRequest request = new EventDetailRequest(event.getName(), + event.getLocation(), event.getInformationUrl(), event.getEventPeriod().getStartDate(), event.getEventPeriod().getEndDate(), - event.getEventPeriod().getApplyStartDate(), event.getEventPeriod().getApplyEndDate(), tags, - event.getImageUrl(), event.getType(), EventMode.OFFLINE, PaymentType.PAID, null, "행사기관"); + event.getEventPeriod().getApplyStartDate(), event.getEventPeriod().getApplyEndDate(), + tags, event.getImageUrl(), event.getType(), EventMode.ON_OFFLINE, PaymentType.FREE, + "행사기관"); - final EventDetailResponse response = new EventDetailResponse(eventId, request.getName(), + final EventDetailResponse response = new EventDetailResponse(1L, request.getName(), request.getInformationUrl(), request.getStartDateTime(), request.getEndDateTime(), request.getApplyStartDateTime(), request.getApplyEndDateTime(), request.getLocation(), EventStatus.IN_PROGRESS.name(), EventStatus.ENDED.name(), - tags.stream().map(TagRequest::getName).collect(Collectors.toList()), request.getImageUrl(), - 10, 10, request.getType().toString(), Collections.emptyList(), "행사기관", "유료"); - - Mockito.when(eventService.updateEvent(any(), any(), - any())).thenReturn(response); - - final RequestFieldsSnippet requestFieldsSnippet = requestFields( - fieldWithPath("name").description("행사(Event) 이름"), - fieldWithPath("location").description("행사(Event) 장소"), - fieldWithPath("startDateTime").description("행사(Event) 시작일시"), - fieldWithPath("endDateTime").description("행사(Event) 종료일시"), - fieldWithPath("applyStartDateTime").description("행사(Event) 신청시작일시"), - fieldWithPath("applyEndDateTime").description("행사(Event) 신청종료일시"), - fieldWithPath("informationUrl").description("행사(Event) 상세 정보 URL"), - fieldWithPath("tags[].name").description("연관 태그명"), - fieldWithPath("imageUrl").description("행사(Event) 이미지url"), - fieldWithPath("type").description("행사(Event) 타입"), - fieldWithPath("eventMode").description("행사 온오프라인 여부(ON_OFFLINE, OFFLINE, ONLINE)"), - fieldWithPath("paymentType").description("행사 유료 여부(PAID, FREE, FREE_PAID)"), - fieldWithPath("images").description("이미지들").optional(), - fieldWithPath("organization").description("행사 기관") + tags.stream().map(TagRequest::getName).collect(Collectors.toList()), + request.getImageUrl(), 10, 10, request.getType().toString(), + List.of("imageUrl1", "imageUrl2"), "행사기관", "유료"); + + Mockito.when(eventService.updateEvent(eq(eventId), any(EventDetailRequest.class), any(), any())) + .thenReturn(response); + + String contents = objectMapper.writeValueAsString(request); + + final RequestPartsSnippet requestPartsSnippet = requestParts( + partWithName("images").description("이미지들").optional(), + partWithName("request").description("행사 정보들"), + partWithName("request.name").description("행사(Event) 이름").optional(), + partWithName("request.location").description("행사(Event) 장소").optional(), + partWithName("request.startDateTime").description("행사(Event) 시작일시").optional(), + partWithName("request.endDateTime").description("행사(Event) 종료일시").optional(), + partWithName("request.applyStartDateTime").description("행사(Event) 신청시작일시").optional(), + partWithName("request.applyEndDateTime").description("행사(Event) 신청종료일시").optional(), + partWithName("request.informationUrl").description("행사(Event) 상세 정보 URL").optional(), + partWithName("request.tags[]").description("연관 태그명").optional(), + partWithName("request.imageUrl").description("행사(Event) imageUrl").optional(), + partWithName("request.type").description("Event 타입").optional(), + partWithName("request.eventMode").description("행사 온오프라인 여부(ON_OFFLINE, OFFLINE, ONLINE)") + .optional(), + partWithName("request.paymentType").description("행사 유료 여부(PAID, FREE, FREE_PAID)") + .optional(), + partWithName("request.organization").description("행사 주최 기관").optional() ); + //when + MockMultipartHttpServletRequestBuilder builder = multipart(HttpMethod.PUT, "/events/" + eventId) + .file("images", image1.getBytes()) + .file("images", image2.getBytes()) + .file(new MockMultipartFile("request", "", "application/json", contents.getBytes( + StandardCharsets.UTF_8))); + + final ResultActions result = mockMvc.perform(builder); + //when & then - mockMvc.perform(put("/events/" + eventId) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) + result.andExpect(status().isOk()) .andDo(MockMvcResultHandlers.print()) - .andDo(MockMvcRestDocumentation.document("update-event", requestFieldsSnippet, + .andDo(MockMvcRestDocumentation.document("update-event", requestPartsSnippet, EVENT_DETAIL_RESPONSE_FILED)); + + } @Test @@ -292,7 +318,7 @@ void addEventTest() throws Exception { event.getEventPeriod().getEndDate(), event.getEventPeriod().getApplyStartDate(), event.getEventPeriod().getApplyEndDate(), tags, event.getImageUrl(), event.getType(), EventMode.ON_OFFLINE, PaymentType.FREE, - List.of(image1, image2), "행사기관"); + "행사기관"); final EventDetailResponse response = new EventDetailResponse(1L, request.getName(), request.getInformationUrl(), request.getStartDateTime(), request.getEndDateTime(), @@ -302,58 +328,45 @@ void addEventTest() throws Exception { request.getImageUrl(), 10, 10, request.getType().toString(), List.of("imageUrl1", "imageUrl2"), "행사기관", "무료"); - Mockito.when(eventService.addEvent(any(), any())) + Mockito.when(eventService.addEvent(any(EventDetailRequest.class), any(), any())) .thenReturn(response); - final RequestParametersSnippet requestParam = requestParameters( - parameterWithName("name").description("행사(Event) 이름"), - parameterWithName("location").description("행사(Event) 장소"), - parameterWithName("startDateTime").description("행사(Event) 시작일시"), - parameterWithName("endDateTime").description("행사(Event) 종료일시"), - parameterWithName("applyStartDateTime").description("행사(Event) 신청시작일시"), - parameterWithName("applyEndDateTime").description("행사(Event) 신청종료일시"), - parameterWithName("informationUrl").description("행사(Event) 상세 정보 URL"), - parameterWithName("tags").description("연관 태그명"), - parameterWithName("imageUrl").description("행사(Event) imageUrl"), - parameterWithName("type").description("Event 타입"), - parameterWithName("eventMode").description("행사 온오프라인 여부(ON_OFFLINE, OFFLINE, ONLINE)"), - parameterWithName("paymentType").description("행사 유료 여부(PAID, FREE, FREE_PAID)") - ); + String contents = objectMapper.writeValueAsString(request); final RequestPartsSnippet requestPartsSnippet = requestParts( - partWithName("images").description("이미지들").optional() + partWithName("images").description("이미지들").optional(), + partWithName("request").description("행사 정보들"), + partWithName("request.name").description("행사(Event) 이름").optional(), + partWithName("request.location").description("행사(Event) 장소").optional(), + partWithName("request.startDateTime").description("행사(Event) 시작일시").optional(), + partWithName("request.endDateTime").description("행사(Event) 종료일시").optional(), + partWithName("request.applyStartDateTime").description("행사(Event) 신청시작일시").optional(), + partWithName("request.applyEndDateTime").description("행사(Event) 신청종료일시").optional(), + partWithName("request.informationUrl").description("행사(Event) 상세 정보 URL").optional(), + partWithName("request.tags[]").description("연관 태그명").optional(), + partWithName("request.imageUrl").description("행사(Event) imageUrl").optional(), + partWithName("request.type").description("Event 타입").optional(), + partWithName("request.eventMode").description("행사 온오프라인 여부(ON_OFFLINE, OFFLINE, ONLINE)") + .optional(), + partWithName("request.paymentType").description("행사 유료 여부(PAID, FREE, FREE_PAID)") + .optional(), + partWithName("request.organization").description("행사 주최 기관").optional() ); //when MockMultipartHttpServletRequestBuilder builder = multipart("/events") .file("images", image1.getBytes()) - .file("images", image2.getBytes()); - - builder.param("name", request.getName()) - .param("location", request.getLocation()) - .param("informationUrl", request.getInformationUrl()) - .param("startDateTime", - request.getStartDateTime().format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("endDateTime", - request.getEndDateTime().format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("applyStartDateTime", request.getApplyStartDateTime() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("applyEndDateTime", request.getApplyEndDateTime() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("tags", request.getTags().stream().map(TagRequest::getName) - .collect(Collectors.joining(","))) - .param("imageUrl", request.getImageUrl()) - .param("type", request.getType().toString()) - .param("eventMode", request.getEventMode().toString()) - .param("paymentType", request.getPaymentType().toString()); + .file("images", image2.getBytes()) + .file(new MockMultipartFile("request", "", "application/json", contents.getBytes( + StandardCharsets.UTF_8))); final ResultActions result = mockMvc.perform(builder); //then result.andExpect(status().isCreated()) .andDo(MockMvcResultHandlers.print()) - .andDo(MockMvcRestDocumentation.document("add-event", requestParam, - EVENT_DETAIL_RESPONSE_FILED, requestPartsSnippet)); + .andDo(MockMvcRestDocumentation.document("add-event", EVENT_DETAIL_RESPONSE_FILED, + requestPartsSnippet)); } @ParameterizedTest @@ -362,30 +375,38 @@ void addEventTest() throws Exception { @DisplayName("이름에 빈 값이 들어올 경우 400 BAD_REQUEST를 반환한다.") void addEventWithEmptyNameTest(final String eventName) throws Exception { //given + final MockMultipartFile image1 = new MockMultipartFile( + "picture", + "picture.jpg", + MediaType.TEXT_PLAIN_VALUE, + "test data".getBytes() + ); + + final MockMultipartFile image2 = new MockMultipartFile( + "picture", + "picture.jpg", + MediaType.TEXT_PLAIN_VALUE, + "test data".getBytes() + ); + final Event event = EventFixture.인프콘_2023(); final List tags = Stream.of(TagFixture.백엔드(), TagFixture.안드로이드()) .map(tag -> new TagRequest(tag.getName())).collect(Collectors.toList()); - + final EventDetailRequest request = new EventDetailRequest( + eventName, event.getLocation(), event.getInformationUrl(), event.getEventPeriod() + .getStartDate(), event.getEventPeriod().getEndDate(), + event.getEventPeriod().getApplyStartDate(), event.getEventPeriod().getApplyEndDate(), + tags, event.getImageUrl(), event.getType(), event.getEventMode(), + event.getPaymentType(), event.getOrganization()); + String contents = objectMapper.writeValueAsString(request); //when & then - mockMvc.perform(post("/events") - .param("name", eventName) - .param("location", event.getLocation()) - .param("informationUrl", event.getInformationUrl()) - .param("startDateTime", event.getEventPeriod().getStartDate() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("endDateTime", event.getEventPeriod().getEndDate() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("applyStartDateTime", event.getEventPeriod().getApplyStartDate() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("applyEndDateTime", event.getEventPeriod().getApplyEndDate() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("imageUrl", event.getImageUrl()) - .param("type", event.getType().toString()) - .param("eventMode", EventMode.ON_OFFLINE.toString()) - .param("paymentType", PaymentType.FREE.toString()) - .param("tags", tags.stream().map(TagRequest::getName) - .collect(Collectors.joining(",")))) + mockMvc.perform(multipart("/events") + .file("images", image1.getBytes()) + .file("images", image2.getBytes()) + .file(new MockMultipartFile("request", "", "application/json", contents.getBytes( + StandardCharsets.UTF_8))) + ) .andExpect(status().isBadRequest()); } @@ -395,30 +416,38 @@ void addEventWithEmptyNameTest(final String eventName) throws Exception { @DisplayName("장소에 빈 값이 들어올 경우 400 BAD_REQUEST를 반환한다.") void addEventWithEmptyLocationTest(final String eventLocation) throws Exception { //given + final MockMultipartFile image1 = new MockMultipartFile( + "picture", + "picture.jpg", + MediaType.TEXT_PLAIN_VALUE, + "test data".getBytes() + ); + + final MockMultipartFile image2 = new MockMultipartFile( + "picture", + "picture.jpg", + MediaType.TEXT_PLAIN_VALUE, + "test data".getBytes() + ); + final Event event = EventFixture.인프콘_2023(); final List tags = Stream.of(TagFixture.백엔드(), TagFixture.안드로이드()) .map(tag -> new TagRequest(tag.getName())).collect(Collectors.toList()); - + final EventDetailRequest request = new EventDetailRequest( + event.getName(), eventLocation, event.getInformationUrl(), event.getEventPeriod() + .getStartDate(), event.getEventPeriod().getEndDate(), + event.getEventPeriod().getApplyStartDate(), event.getEventPeriod().getApplyEndDate(), + tags, event.getImageUrl(), event.getType(), event.getEventMode(), + event.getPaymentType(), event.getOrganization()); + String contents = objectMapper.writeValueAsString(request); //when & then - mockMvc.perform(post("/events") - .param("name", event.getName()) - .param("location", eventLocation) - .param("informationUrl", eventLocation) - .param("startDateTime", event.getEventPeriod().getStartDate() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("endDateTime", event.getEventPeriod().getEndDate() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("applyStartDateTime", event.getEventPeriod().getApplyStartDate() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("applyEndDateTime", event.getEventPeriod().getApplyEndDate() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("imageUrl", event.getImageUrl()) - .param("type", event.getType().toString()) - .param("eventMode", EventMode.ON_OFFLINE.toString()) - .param("paymentType", PaymentType.FREE.toString()) - .param("tags", tags.stream().map(TagRequest::getName) - .collect(Collectors.joining(",")))) + mockMvc.perform(multipart("/events") + .file("images", image1.getBytes()) + .file("images", image2.getBytes()) + .file(new MockMultipartFile("request", "", "application/json", contents.getBytes( + StandardCharsets.UTF_8))) + ) .andExpect(status().isBadRequest()); } @@ -429,32 +458,39 @@ void addEventWithEmptyLocationTest(final String eventLocation) throws Exception @DisplayName("상세 URL에 http:// 혹은 https://로 시작하지 않는 값이 들어올 경우 400 BAD_REQUEST를 반환한다.") void addEventWithInvalidInformationUrlTest(final String informationUrl) throws Exception { //given + final MockMultipartFile image1 = new MockMultipartFile( + "picture", + "picture.jpg", + MediaType.TEXT_PLAIN_VALUE, + "test data".getBytes() + ); + + final MockMultipartFile image2 = new MockMultipartFile( + "picture", + "picture.jpg", + MediaType.TEXT_PLAIN_VALUE, + "test data".getBytes() + ); + final Event event = EventFixture.인프콘_2023(); final List tags = Stream.of(TagFixture.백엔드(), TagFixture.안드로이드()) .map(tag -> new TagRequest(tag.getName())).collect(Collectors.toList()); - - //when - mockMvc.perform(post("/events") - .param("name", event.getName()) - .param("location", event.getLocation()) - .param("informationUrl", informationUrl) - .param("startDateTime", event.getEventPeriod().getStartDate() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("endDateTime", event.getEventPeriod().getEndDate() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("applyStartDateTime", event.getEventPeriod().getApplyStartDate() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("applyEndDateTime", event.getEventPeriod().getApplyEndDate() - .format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm:ss"))) - .param("imageUrl", event.getImageUrl()) - .param("type", event.getType().toString()) - .param("eventMode", EventMode.ON_OFFLINE.toString()) - .param("paymentType", PaymentType.FREE.toString()) - .param("tags", tags.stream().map(TagRequest::getName) - .collect(Collectors.joining(",")))) + final EventDetailRequest request = new EventDetailRequest( + event.getName(), event.getLocation(), informationUrl, event.getEventPeriod() + .getStartDate(), event.getEventPeriod().getEndDate(), + event.getEventPeriod().getApplyStartDate(), event.getEventPeriod().getApplyEndDate(), + tags, event.getImageUrl(), event.getType(), event.getEventMode(), + event.getPaymentType(), event.getOrganization()); + String contents = objectMapper.writeValueAsString(request); + //when & then + mockMvc.perform(multipart("/events") + .file("images", image1.getBytes()) + .file("images", image2.getBytes()) + .file(new MockMultipartFile("request", "", "application/json", contents.getBytes( + StandardCharsets.UTF_8))) + ) .andExpect(status().isBadRequest()); - } @ParameterizedTest @@ -462,16 +498,47 @@ void addEventWithInvalidInformationUrlTest(final String informationUrl) throws E "2023-01-01T2:00:00", "2023-01-01T12:0:00", "2023-01-01T12:00:0"}) @NullSource @DisplayName("시작 일시에 null 혹은 다른 형식의 일시 값이 들어올 경우 400 BAD_REQUEST를 반환한다.") - void addEventWithUnformattedStartDateTimeTest(final String startDateTime) throws Exception { + void addEventWithUnformattedStartDateTimeTest(final String startDateTime) + throws Exception { //when & then - mockMvc.perform(post("/events").contentType(MediaType.APPLICATION_JSON_VALUE) - .param("name", "인프콘 2023") - .param("location", "코엑스") - .param("informationUrl", "https://~~~") - .param("startDateTime", startDateTime) // 이 변수의 값을 확인하고 올바른 값을 설정하세요. - .param("endDateTime", "2023-01-02T12:00:00") - .param("tags[0].name", "백엔드") - .param("tags[1].name", "안드로이드")) + final MockMultipartFile image1 = new MockMultipartFile( + "picture", + "picture.jpg", + MediaType.TEXT_PLAIN_VALUE, + "test data".getBytes() + ); + + final MockMultipartFile image2 = new MockMultipartFile( + "picture", + "picture.jpg", + MediaType.TEXT_PLAIN_VALUE, + "test data".getBytes() + ); + + final Event event = EventFixture.인프콘_2023(); + + Map request = new HashMap<>(); + request.put("name", event.getName()); + request.put("location", event.getLocation()); + request.put("informationUrl", event.getInformationUrl()); + request.put("startDateTime", startDateTime); + request.put("endDateTime", event.getEventPeriod().getEndDate().toString()); + request.put("applyStartDateTime", event.getEventPeriod().getApplyStartDate().toString()); + request.put("applyEndDateTime", event.getEventPeriod().getApplyEndDate().toString()); + request.put("imageUrl", event.getImageUrl()); + request.put("type", event.getType().name()); + request.put("eventMode", event.getEventMode().name()); + request.put("paymentType", event.getPaymentType().name()); + request.put("organization", event.getOrganization()); + + String contents = objectMapper.writeValueAsString(request); + //when & then + mockMvc.perform(multipart("/events") + .file("images", image1.getBytes()) + .file("images", image2.getBytes()) + .file(new MockMultipartFile("request", "", "application/json", contents.getBytes( + StandardCharsets.UTF_8))) + ) .andExpect(status().isBadRequest()); } @@ -482,16 +549,45 @@ void addEventWithUnformattedStartDateTimeTest(final String startDateTime) throws @DisplayName("종료 일시에 null 혹은 다른 형식의 일시 값이 들어올 경우 400 BAD_REQUEST를 반환한다.") void addEventWithUnformattedEndDateTimeTest(final String endDateTime) throws Exception { //when & then - mockMvc.perform(post("/events") - .param("name", "인프콘 2023") - .param("location", "코엑스") - .param("informationUrl", "https://~~~") - .param("startDateTime", "2023-01-01T12:00:00") - .param("endDateTime", endDateTime) // 이 변수의 값을 확인하고 올바른 값을 설정하세요. - .param("tags[0].name", "백엔드") - .param("tags[1].name", "안드로이드")) + final MockMultipartFile image1 = new MockMultipartFile( + "picture", + "picture.jpg", + MediaType.TEXT_PLAIN_VALUE, + "test data".getBytes() + ); + + final MockMultipartFile image2 = new MockMultipartFile( + "picture", + "picture.jpg", + MediaType.TEXT_PLAIN_VALUE, + "test data".getBytes() + ); + + final Event event = EventFixture.인프콘_2023(); + + Map request = new HashMap<>(); + request.put("name", event.getName()); + request.put("location", event.getLocation()); + request.put("informationUrl", event.getInformationUrl()); + request.put("startDateTime", event.getEventPeriod().getStartDate().toString()); + request.put("endDateTime", endDateTime); + request.put("applyStartDateTime", event.getEventPeriod().getApplyStartDate().toString()); + request.put("applyEndDateTime", event.getEventPeriod().getApplyEndDate().toString()); + request.put("imageUrl", event.getImageUrl()); + request.put("type", event.getType().name()); + request.put("eventMode", event.getEventMode().name()); + request.put("paymentType", event.getPaymentType().name()); + request.put("organization", event.getOrganization()); + + String contents = objectMapper.writeValueAsString(request); + //when & then + mockMvc.perform(multipart("/events") + .file("images", image1.getBytes()) + .file("images", image2.getBytes()) + .file(new MockMultipartFile("request", "", "application/json", contents.getBytes( + StandardCharsets.UTF_8))) + ) .andExpect(status().isBadRequest()); } } - } diff --git a/backend/emm-sale/src/main/java/com/emmsale/event/api/EventApi.java b/backend/emm-sale/src/main/java/com/emmsale/event/api/EventApi.java index 41abaf787..c95630d3e 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/event/api/EventApi.java +++ b/backend/emm-sale/src/main/java/com/emmsale/event/api/EventApi.java @@ -11,17 +11,19 @@ import javax.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/events") @@ -46,17 +48,19 @@ public ResponseEntity> findEvents( eventService.findEvents(category, LocalDate.now(), startDate, endDate, tags, statuses)); } - @PostMapping + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @ResponseStatus(HttpStatus.CREATED) - public EventDetailResponse addEvent(@Valid final EventDetailRequest request) { - return eventService.addEvent(request, LocalDate.now()); + public EventDetailResponse addEvent(@RequestPart @Valid final EventDetailRequest request, + @RequestPart final List images) { + return eventService.addEvent(request, images, LocalDate.now()); } - @PutMapping("/{eventId}") + @PutMapping(path = "/{eventId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @ResponseStatus(HttpStatus.OK) public EventDetailResponse updateEvent(@PathVariable final Long eventId, - @RequestBody @Valid final EventDetailRequest request) { - return eventService.updateEvent(eventId, request, LocalDate.now()); + @RequestPart @Valid final EventDetailRequest request, + @RequestPart final List images) { + return eventService.updateEvent(eventId, request, images, LocalDate.now()); } @DeleteMapping("/{eventId}") diff --git a/backend/emm-sale/src/main/java/com/emmsale/event/application/EventService.java b/backend/emm-sale/src/main/java/com/emmsale/event/application/EventService.java index 16af445b5..702f5d586 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/event/application/EventService.java +++ b/backend/emm-sale/src/main/java/com/emmsale/event/application/EventService.java @@ -38,6 +38,7 @@ import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; @Service @Transactional @@ -178,13 +179,14 @@ private List filterEventResponseByStatuses( }); } - public EventDetailResponse addEvent(final EventDetailRequest request, final LocalDate today) { + public EventDetailResponse addEvent(final EventDetailRequest request, + final List images, final LocalDate today) { final Event event = eventRepository.save(request.toEvent()); final List tags = findAllPersistTagsOrElseThrow(request.getTags()); event.addAllEventTags(tags); final List imageUrls = imageCommandService - .saveImages(ImageType.EVENT, event.getId(), request.getImages()) + .saveImages(ImageType.EVENT, event.getId(), images) .stream() .sorted(comparing(Image::getOrder)) .map(Image::getName) @@ -196,7 +198,7 @@ public EventDetailResponse addEvent(final EventDetailRequest request, final Loca } public EventDetailResponse updateEvent(final Long eventId, final EventDetailRequest request, - final LocalDate today) { + final List images, final LocalDate today) { final Event event = eventRepository.findById(eventId) .orElseThrow(() -> new EventException(NOT_FOUND_EVENT)); diff --git a/backend/emm-sale/src/main/java/com/emmsale/event/application/dto/EventDetailRequest.java b/backend/emm-sale/src/main/java/com/emmsale/event/application/dto/EventDetailRequest.java index 77e8e2819..e82c8fcfe 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/event/application/dto/EventDetailRequest.java +++ b/backend/emm-sale/src/main/java/com/emmsale/event/application/dto/EventDetailRequest.java @@ -14,7 +14,6 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.multipart.MultipartFile; @RequiredArgsConstructor @Getter @@ -51,8 +50,6 @@ public class EventDetailRequest { private final EventMode eventMode; private final PaymentType paymentType; - private final List images; - private final String organization; public Event toEvent() { diff --git a/backend/emm-sale/src/main/java/com/emmsale/image/domain/ImageType.java b/backend/emm-sale/src/main/java/com/emmsale/image/domain/ImageType.java index 49ede5292..8e0f876f3 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/image/domain/ImageType.java +++ b/backend/emm-sale/src/main/java/com/emmsale/image/domain/ImageType.java @@ -2,16 +2,20 @@ public enum ImageType { FEED(5), - EVENT(2); - + EVENT(0); + + private static final int NO_LIMIT_COUNT = 0; private final int maxImageCount; - + ImageType(final int maxImageCount) { this.maxImageCount = maxImageCount; } - + public boolean isOverMaxImageCount(final int imageCount) { + if (maxImageCount == NO_LIMIT_COUNT) { + return false; + } return imageCount > maxImageCount; } - + } diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/domain/InterestTagRepository.java b/backend/emm-sale/src/main/java/com/emmsale/member/domain/InterestTagRepository.java index 0ce44b05a..71e5ee471 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/member/domain/InterestTagRepository.java +++ b/backend/emm-sale/src/main/java/com/emmsale/member/domain/InterestTagRepository.java @@ -6,20 +6,23 @@ import org.springframework.data.repository.query.Param; public interface InterestTagRepository extends JpaRepository { - - List findInterestTagsByMemberId(final Long memberId); - + + @Query("select it from InterestTag it " + + "join fetch it.tag " + + "where it.member.id = :memberId") + List findInterestTagsByMemberId(@Param("memberId") final Long memberId); + boolean existsByTagIdIn(List tagIds); - + @Query("select it from InterestTag it " + "where it.member = :member " + "and it.tag.id in :deleteTagId") List findAllByMemberAndTagIds( @Param("member") final Member member, @Param("deleteTagId") final List deleteTagId); - + @Query("select it from InterestTag it join fetch it.tag where it.tag.id in :ids") List findInterestTagsByTagIdIn(@Param("ids") final List ids); - + void deleteAllByMember(Member member); } diff --git a/backend/emm-sale/src/test/java/com/emmsale/event/application/EventServiceTest.java b/backend/emm-sale/src/test/java/com/emmsale/event/application/EventServiceTest.java index 20ab82a78..3de8d1caa 100644 --- a/backend/emm-sale/src/test/java/com/emmsale/event/application/EventServiceTest.java +++ b/backend/emm-sale/src/test/java/com/emmsale/event/application/EventServiceTest.java @@ -543,14 +543,13 @@ void addEventTest() { type, eventMode, paymentType, - mockMultipartFiles, organization ); doNothing().when(firebaseCloudMessageClient).sendMessageTo(any(UpdateNotification.class)); //when - final EventDetailResponse response = eventService.addEvent(request, now); + final EventDetailResponse response = eventService.addEvent(request, mockMultipartFiles, now); final Event savedEvent = eventRepository.findById(response.getId()).get(); //then @@ -589,7 +588,6 @@ void addEventWithStartDateTimeAfterBeforeDateTimeTest() { type, eventMode, paymentType, - mockMultipartFiles, organization ); @@ -597,7 +595,7 @@ void addEventWithStartDateTimeAfterBeforeDateTimeTest() { //when & then final EventException exception = assertThrowsExactly(EventException.class, - () -> eventService.addEvent(request, now)); + () -> eventService.addEvent(request, mockMultipartFiles, now)); assertEquals(exception.exceptionType(), START_DATE_TIME_AFTER_END_DATE_TIME); } @@ -625,7 +623,6 @@ void addEventWithNotExistTagTest() { type, eventMode, paymentType, - mockMultipartFiles, organization ); @@ -633,7 +630,7 @@ void addEventWithNotExistTagTest() { //when & then final EventException exception = assertThrowsExactly(EventException.class, - () -> eventService.addEvent(request, now)); + () -> eventService.addEvent(request, mockMultipartFiles, now)); assertEquals(exception.exceptionType(), NOT_FOUND_TAG); } @@ -677,7 +674,6 @@ void updateEventTest() { EventType.CONFERENCE, eventMode, paymentType, - mockMultipartFiles, organization ); @@ -685,7 +681,8 @@ void updateEventTest() { final Long eventId = event.getId(); //when - final EventDetailResponse response = eventService.updateEvent(eventId, updateRequest, now); + final EventDetailResponse response = eventService.updateEvent(eventId, updateRequest, + mockMultipartFiles, now); final Event updatedEvent = eventRepository.findById(eventId).get(); //then @@ -723,13 +720,12 @@ void updateEventWithNotExistsEventTest() { EventType.CONFERENCE, eventMode, paymentType, - mockMultipartFiles, organization ); //when & then final EventException exception = assertThrowsExactly(EventException.class, - () -> eventService.updateEvent(notExistsEventId, updateRequest, now)); + () -> eventService.updateEvent(notExistsEventId, updateRequest, mockMultipartFiles, now)); assertEquals(exception.exceptionType(), NOT_FOUND_EVENT); } @@ -754,7 +750,6 @@ void updateEventWithStartDateTimeAfterBeforeDateTimeTest() { EventType.CONFERENCE, eventMode, paymentType, - mockMultipartFiles, organization ); @@ -763,7 +758,7 @@ void updateEventWithStartDateTimeAfterBeforeDateTimeTest() { //when & then final EventException exception = assertThrowsExactly(EventException.class, - () -> eventService.updateEvent(eventId, updateRequest, now)); + () -> eventService.updateEvent(eventId, updateRequest, mockMultipartFiles, now)); assertEquals(exception.exceptionType(), START_DATE_TIME_AFTER_END_DATE_TIME); } @@ -789,7 +784,6 @@ void updateEventWithNotExistTagTest() { EventType.CONFERENCE, eventMode, paymentType, - mockMultipartFiles, organization ); @@ -798,7 +792,7 @@ void updateEventWithNotExistTagTest() { //when & then final EventException exception = assertThrowsExactly(EventException.class, - () -> eventService.updateEvent(eventId, updateRequest, now)); + () -> eventService.updateEvent(eventId, updateRequest, mockMultipartFiles, now)); assertEquals(exception.exceptionType(), NOT_FOUND_TAG); } diff --git a/backend/emm-sale/src/test/java/com/emmsale/image/application/ImageCommandServiceTest.java b/backend/emm-sale/src/test/java/com/emmsale/image/application/ImageCommandServiceTest.java index 9f2797937..39f0e5d0a 100644 --- a/backend/emm-sale/src/test/java/com/emmsale/image/application/ImageCommandServiceTest.java +++ b/backend/emm-sale/src/test/java/com/emmsale/image/application/ImageCommandServiceTest.java @@ -1,6 +1,7 @@ package com.emmsale.image.application; import static com.emmsale.event.EventFixture.인프콘_2023; +import static com.emmsale.member.MemberFixture.memberFixture; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; @@ -13,6 +14,7 @@ import com.emmsale.event.domain.repository.EventRepository; import com.emmsale.event.exception.EventException; import com.emmsale.event.exception.EventExceptionType; +import com.emmsale.feed.domain.Feed; import com.emmsale.feed.domain.repository.FeedRepository; import com.emmsale.feed.exception.FeedException; import com.emmsale.feed.exception.FeedExceptionType; @@ -22,6 +24,8 @@ import com.emmsale.image.domain.repository.ImageRepository; import com.emmsale.image.exception.ImageException; import com.emmsale.image.exception.ImageExceptionType; +import com.emmsale.member.domain.Member; +import com.emmsale.member.domain.MemberRepository; import java.util.List; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.BeforeEach; @@ -34,7 +38,7 @@ import org.springframework.web.multipart.MultipartFile; class ImageCommandServiceTest extends ServiceIntegrationTestHelper { - + private ImageCommandService imageCommandService; private ImageCommandService imageCommandServiceWithMockImageRepository; @Autowired @@ -44,8 +48,10 @@ class ImageCommandServiceTest extends ServiceIntegrationTestHelper { private EventRepository eventRepository; @Autowired private FeedRepository feedRepository; + @Autowired + private MemberRepository memberRepository; private S3Client s3Client; - + @BeforeEach void setUp() { s3Client = mock(S3Client.class); @@ -63,11 +69,11 @@ void setUp() { feedRepository ); } - + @Nested @DisplayName("saveImages() 메서드를 호출하면 S3와 DB에 이미지를 업로드한다.") class SaveImages { - + @Test @DisplayName("S3와 DB에 Image를 성공적으로 업로드할 수 있다.") void saveImages_success() { @@ -80,14 +86,14 @@ void saveImages_success() { final List files = List.of( new MockMultipartFile("test", "test.png", "", new byte[]{}), new MockMultipartFile("test", "test.png", "", new byte[]{})); - + BDDMockito.given(s3Client.uploadImages(any())) .willReturn(imageNames); - + //when imageCommandService.saveImages(ImageType.EVENT, event.getId(), files); final List actual = imageRepository.findAll(); - + //then assertAll( () -> assertThat(actual) @@ -98,7 +104,7 @@ void saveImages_success() { .uploadImages(any()) ); } - + @Test @DisplayName("이미지를 추가하려는 행사가 존재하지 않는 행사인 경우 예외를 던진다.") void saveImages_fail_not_found_event() { @@ -107,16 +113,16 @@ void saveImages_fail_not_found_event() { final List files = List.of( new MockMultipartFile("test", "test.png", "", new byte[]{}), new MockMultipartFile("test", "test.png", "", new byte[]{})); - + //when final ThrowingCallable actual = () -> imageCommandService.saveImages(ImageType.EVENT, noExistEventId, files); - + //then assertThatThrownBy(actual).isInstanceOf(EventException.class) .hasMessage(EventExceptionType.NOT_FOUND_EVENT.errorMessage()); } - + @Test @DisplayName("이미지를 추가하려는 행사가 존재하지 않는 피드인 경우 예외를 던진다.") void saveImages_fail_not_found_feed() { @@ -125,36 +131,40 @@ void saveImages_fail_not_found_feed() { final List files = List.of( new MockMultipartFile("test", "test.png", "", new byte[]{}), new MockMultipartFile("test", "test.png", "", new byte[]{})); - + //when final ThrowingCallable actual = () -> imageCommandService.saveImages(ImageType.FEED, noExistFeedId, files); - + //then assertThatThrownBy(actual).isInstanceOf(FeedException.class) .hasMessage(FeedExceptionType.NOT_FOUND_FEED.errorMessage()); } - + @Test @DisplayName("추가하려는 이미지의 개수가 컨텐츠의 최대 이미지 개수보다 크면 예외를 던진다.") void saveImages_fail_over_max_image_count() { //given final Event event = eventRepository.save(인프콘_2023()); + final Member member = memberRepository.save(memberFixture()); + final Feed feed = feedRepository.save(new Feed(event, member, "피드", "피드 내용")); final List files = List.of( new MockMultipartFile("test", "test1.png", "", new byte[]{}), new MockMultipartFile("test", "test2.png", "", new byte[]{}), new MockMultipartFile("test", "test3.png", "", new byte[]{}), - new MockMultipartFile("test", "test4.png", "", new byte[]{})); - + new MockMultipartFile("test", "test4.png", "", new byte[]{}), + new MockMultipartFile("test", "test5.png", "", new byte[]{}), + new MockMultipartFile("test", "test6.png", "", new byte[]{})); + //when - final ThrowingCallable actual = () -> imageCommandService.saveImages(ImageType.EVENT, - event.getId(), files); - + final ThrowingCallable actual = () -> imageCommandService.saveImages(ImageType.FEED, + feed.getId(), files); + //then assertThatThrownBy(actual).isInstanceOf(ImageException.class) .hasMessage(ImageExceptionType.OVER_MAX_IMAGE_COUNT.errorMessage()); } - + @Test @DisplayName("이미지를 DB에 저장하는 작업이 실패하면 S3에 저장된 이미지를 삭제하고 예외를 던진다.") void saveImages_fail_and_rollback() { @@ -164,17 +174,17 @@ void saveImages_fail_and_rollback() { final List files = List.of( new MockMultipartFile("test", "test.png", "", new byte[]{}), new MockMultipartFile("test", "test.png", "", new byte[]{})); - + BDDMockito.given(s3Client.uploadImages(any())) .willReturn(imageNames); BDDMockito.willDoNothing().given(s3Client).deleteImages(any()); BDDMockito.given(mockImageRepository.save(any(Image.class))) .willThrow(new IllegalArgumentException()); - + //when final ThrowingCallable actual = () -> imageCommandServiceWithMockImageRepository.saveImages( ImageType.EVENT, event.getId(), files); - + //then assertThatThrownBy(actual) .isInstanceOf(ImageException.class) @@ -187,11 +197,11 @@ void saveImages_fail_and_rollback() { ); } } - + @Nested @DisplayName("deleteImages() 메서드를 호출하면 특정 컨텐츠의 이미지들을 S3와 DB에서 삭제한다.") class DeleteImages { - + @Test @DisplayName("S3와 DB에서 Image를 성공적으로 삭제할 수 있다.") void deleteImages_success() { @@ -201,13 +211,13 @@ void deleteImages_success() { new Image("테스트테스트.png", ImageType.EVENT, event.getId(), 0, null), new Image("테스트테스트2.png", ImageType.EVENT, event.getId(), 1, null)); imageRepository.saveAll(expected); - + BDDMockito.willDoNothing().given(s3Client).deleteImages(any()); - + //when imageCommandService.deleteImages(ImageType.EVENT, event.getId()); final List actual = imageRepository.findAll(); - + //then assertAll( () -> assertThat(actual).isEmpty(), diff --git a/backend/emm-sale/src/test/java/com/emmsale/image/domain/ImageTypeTest.java b/backend/emm-sale/src/test/java/com/emmsale/image/domain/ImageTypeTest.java index 2153b79a9..6d36af768 100644 --- a/backend/emm-sale/src/test/java/com/emmsale/image/domain/ImageTypeTest.java +++ b/backend/emm-sale/src/test/java/com/emmsale/image/domain/ImageTypeTest.java @@ -7,14 +7,14 @@ import org.junit.jupiter.params.provider.CsvSource; class ImageTypeTest { - + @ParameterizedTest - @CsvSource(value = {"FEED:3:false", "EVENT:3:true"}, delimiter = ':') + @CsvSource(value = {"FEED:3:false", "FEED:6:true", "EVENT:999:false"}, delimiter = ':') @DisplayName("isOverMaxImageCount(): 입력받은 값이 이미지 유형의 최대 이미지 수보다 큰지 여부를 반환한다.") void isOverMaxImageCount(final ImageType type, final int imageCount, final boolean expected) { //given, when final boolean actual = type.isOverMaxImageCount(imageCount); - + //then assertThat(actual).isEqualTo(expected); }