diff --git a/backend/emm-sale/src/docs/asciidoc/index.adoc b/backend/emm-sale/src/docs/asciidoc/index.adoc index 8fbbc435b..59730bc33 100644 --- a/backend/emm-sale/src/docs/asciidoc/index.adoc +++ b/backend/emm-sale/src/docs/asciidoc/index.adoc @@ -106,7 +106,6 @@ include::{snippets}/find-profile/http-response.adoc[] .HTTP response 설명 include::{snippets}/find-profile/response-fields.adoc[] - == Event === `GET` : 행사 상세정보 조회 @@ -153,6 +152,45 @@ include::{snippets}/find-participants/http-response.adoc[] .HTTP response 설명 include::{snippets}/find-participants/response-fields.adoc[] +=== `POST` : 이벤트 생성 + +.HTTP request +include::{snippets}/add-event/http-request.adoc[] + +.HTTP request 설명 +include::{snippets}/add-event/request-fields.adoc[] + +.HTTP response +include::{snippets}/add-event/http-response.adoc[] + +.HTTP response 설명 +include::{snippets}/add-event/response-fields.adoc[] + +=== `PUT` : 이벤트 업데이트 + +.HTTP request +include::{snippets}/update-event/http-request.adoc[] + +.HTTP request 설명 +include::{snippets}/update-event/request-fields.adoc[] + +.HTTP response +include::{snippets}/update-event/http-response.adoc[] + +.HTTP response 설명 +include::{snippets}/update-event/response-fields.adoc[] + +=== `PUT` : 이벤트 삭제 + +.HTTP request +include::{snippets}/delete-event/http-request.adoc[] + +.HTTP response +include::{snippets}/delete-event/http-response.adoc[] + +.HTTP response 설명 +include::{snippets}/delete-event/response-fields.adoc[] + == Comment === `GET` : 댓글 모두 조회 diff --git a/backend/emm-sale/src/main/java/com/emmsale/GlobalControllerAdvice.java b/backend/emm-sale/src/main/java/com/emmsale/GlobalControllerAdvice.java index ce5921eee..16c3835db 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/GlobalControllerAdvice.java +++ b/backend/emm-sale/src/main/java/com/emmsale/GlobalControllerAdvice.java @@ -2,8 +2,13 @@ import com.emmsale.base.BaseException; import com.emmsale.base.BaseExceptionType; +import java.time.format.DateTimeParseException; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -18,6 +23,24 @@ public ResponseEntity handleException(final BaseException e) return new ResponseEntity<>(ExceptionResponse.from(e), type.httpStatus()); } + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationException( + final MethodArgumentNotValidException e) { + final String message = e.getBindingResult().getAllErrors().stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining("\n")); + log.warn("[WARN] MESSAGE: {}", message); + return new ResponseEntity<>(new ExceptionResponse(message), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(DateTimeParseException.class) + public ResponseEntity handleDateTimeParseException( + final DateTimeParseException e) { + final String message = "DateTime 입력 형식이 올바르지 않습니다."; + log.warn("[WARN] MESSAGE: " + message); + return new ResponseEntity<>(new ExceptionResponse(message), HttpStatus.BAD_REQUEST); + } + static class ExceptionResponse { private final String message; 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 55401d11d..1034ed834 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 @@ -4,6 +4,7 @@ import static java.net.URI.create; import com.emmsale.event.application.EventService; +import com.emmsale.event.application.dto.EventDetailRequest; import com.emmsale.event.application.dto.EventDetailResponse; import com.emmsale.event.application.dto.EventParticipateRequest; import com.emmsale.event.application.dto.EventResponse; @@ -11,14 +12,19 @@ import com.emmsale.member.domain.Member; import java.time.LocalDate; import java.util.List; +import javax.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; 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.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController @@ -62,4 +68,24 @@ public ResponseEntity> findEvents(@RequestParam final int ye @RequestParam(required = false) final String status) { return ResponseEntity.ok(eventService.findEvents(LocalDate.now(), year, month, tag, status)); } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public EventDetailResponse addEvent( + @RequestBody @Valid final EventDetailRequest request) { + return eventService.addEvent(request); + } + + @PutMapping("/{event-id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public EventDetailResponse updateEvent(@PathVariable(name = "event-id") final Long eventId, + @RequestBody @Valid final EventDetailRequest request) { + return eventService.updateEvent(eventId, request); + } + + @DeleteMapping("/{event-id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public EventDetailResponse deleteEvent(@PathVariable(name = "event-id") final Long eventId) { + return eventService.deleteEvent(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 ae09a3980..8dcdb768e 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 @@ -9,6 +9,7 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toUnmodifiableList; +import com.emmsale.event.application.dto.EventDetailRequest; import com.emmsale.event.application.dto.EventDetailResponse; import com.emmsale.event.application.dto.EventResponse; import com.emmsale.event.application.dto.ParticipantResponse; @@ -22,10 +23,12 @@ import com.emmsale.event.exception.EventException; import com.emmsale.event.exception.EventExceptionType; import com.emmsale.member.domain.Member; +import com.emmsale.tag.application.dto.TagRequest; import com.emmsale.tag.domain.Tag; import com.emmsale.tag.domain.TagRepository; import com.emmsale.tag.exception.TagException; import java.time.LocalDate; +import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import lombok.RequiredArgsConstructor; @@ -46,6 +49,12 @@ public class EventService { private final EventTagRepository eventTagRepository; private final TagRepository tagRepository; + private static void validateMemberNotAllowed(final Long memberId, final Member member) { + if (member.isNotMe(memberId)) { + throw new EventException(EventExceptionType.FORBIDDEN_PARTICIPATE_EVENT); + } + } + @Transactional(readOnly = true) public EventDetailResponse findEvent(final Long id) { final Event event = eventRepository.findById(id) @@ -63,17 +72,11 @@ public Long participate(final Long eventId, final Long memberId, final Member me return participant.getId(); } - private void validateMemberNotAllowed(final Long memberId, final Member member) { - if (member.isNotMe(memberId)) { - throw new EventException(EventExceptionType.FORBIDDEN_PARTICIPATE_EVENT); - } - } - @Transactional(readOnly = true) public List findEvents(final LocalDate nowDate, final int year, final int month, final String tagName, final String statusName) { validateYearAndMonth(year, month); - List events = filterEventsByTag(tagName); + final List events = filterEventsByTag(tagName); final EnumMap> eventsForEventStatus = groupByEventStatus(nowDate, events, year, month); @@ -101,7 +104,7 @@ private void validateYearAndMonth(final int year, final int month) { private List filterEventsByTag(final String tagName) { if (isExistTagName(tagName)) { - Tag tag = tagRepository.findByName(tagName) + final Tag tag = tagRepository.findByName(tagName) .orElseThrow(() -> new TagException(NOT_FOUND_TAG)); return eventTagRepository.findEventTagsByTag(tag) @@ -131,15 +134,15 @@ private EnumMap> groupByEventStatus(final LocalDate now private boolean isOverlapToMonth(final int year, final int month, final LocalDate eventStart, final LocalDate eventEnd) { - LocalDate monthStart = LocalDate.of(year, month, 1); - LocalDate monthEnd = LocalDate.of(year, month, monthStart.lengthOfMonth()); + final LocalDate monthStart = LocalDate.of(year, month, 1); + final LocalDate monthEnd = LocalDate.of(year, month, monthStart.lengthOfMonth()); return (isBeforeOrEquals(eventStart, monthEnd) && isBeforeOrEquals(monthStart, eventEnd)) || (isBeforeOrEquals(monthStart, eventStart) && isBeforeOrEquals(eventStart, monthEnd)) || (isBeforeOrEquals(monthStart, eventEnd) && isBeforeOrEquals(eventEnd, monthEnd)); } - private boolean isBeforeOrEquals(LocalDate criteria, LocalDate comparison) { + private boolean isBeforeOrEquals(final LocalDate criteria, final LocalDate comparison) { return criteria.isBefore(comparison) || criteria.isEqual(comparison); } @@ -164,5 +167,54 @@ private boolean isExistStatusName(final String statusName) { return statusName != null; } + public EventDetailResponse addEvent(final EventDetailRequest request) { + final Event event = saveNewEvent(request); + + final List tags = findAllPersistTagsOrElseThrow(request.getTags()); + + event.addAllEventTags(tags); + + return EventDetailResponse.from(event); + } + + public EventDetailResponse updateEvent(final Long eventId, final EventDetailRequest request) { + final Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new EventException(NOT_FOUND_EVENT)); + + final List tags = findAllPersistTagsOrElseThrow(request.getTags()); + + eventTagRepository.deleteAllByEventId(eventId); + + final Event updatedEvent = event.updateEventContent(request.getName(), request.getLocation(), + request.getStartDateTime(), request.getEndDateTime(), request.getInformationUrl(), tags); + + return EventDetailResponse.from(updatedEvent); + } + + public EventDetailResponse deleteEvent(final Long eventId) { + final Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new EventException(NOT_FOUND_EVENT)); + + eventRepository.deleteById(eventId); + + return EventDetailResponse.from(event); + } + + private List findAllPersistTagsOrElseThrow(final List tags) { + if (tags == null || tags.isEmpty()) { + return new ArrayList<>(); + } + return tags.stream() + .map(tag -> tagRepository.findByName(tag.getName()) + .orElseThrow(() -> new EventException(EventExceptionType.NOT_FOUND_TAG))) + .collect(toList()); + } + + private Event saveNewEvent(final EventDetailRequest request) { + final Event event = new Event(request.getName(), request.getLocation(), + request.getStartDateTime(), request.getEndDateTime(), request.getInformationUrl()); + + return eventRepository.save(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 new file mode 100644 index 000000000..0f229ebab --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/event/application/dto/EventDetailRequest.java @@ -0,0 +1,35 @@ +package com.emmsale.event.application.dto; + +import com.emmsale.tag.application.dto.TagRequest; +import java.time.LocalDateTime; +import java.util.List; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +@RequiredArgsConstructor +@Getter +public class EventDetailRequest { + + private static final String DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; + + @NotBlank(message = "행사의 이름을 입력해 주세요.") + private final String name; + @NotBlank(message = "행사의 장소를 입력해 주세요.") + private final String location; + @NotBlank(message = "행사의 상세 URL을 입력해 주세요.") + @Pattern(regexp = "(http.?://).*", message = "http:// 혹은 https://로 시작하는 주소를 입력해 주세요.") + private final String informationUrl; + + @DateTimeFormat(pattern = DATE_TIME_FORMAT) + @NotNull(message = "행사의 시작 일시를 입력해 주세요.") + private final LocalDateTime startDateTime; + @DateTimeFormat(pattern = DATE_TIME_FORMAT) + @NotNull(message = "행사의 종료 일시를 입력해 주세요.") + private final LocalDateTime endDateTime; + + private final List tags; +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/event/domain/Event.java b/backend/emm-sale/src/main/java/com/emmsale/event/domain/Event.java index e459f9a45..15a701661 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/event/domain/Event.java +++ b/backend/emm-sale/src/main/java/com/emmsale/event/domain/Event.java @@ -7,13 +7,16 @@ import com.emmsale.event.exception.EventException; import com.emmsale.event.exception.EventExceptionType; import com.emmsale.member.domain.Member; +import com.emmsale.tag.domain.Tag; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @@ -39,7 +42,7 @@ public class Event extends BaseEntity { private LocalDateTime endDate; @Column(nullable = false) private String informationUrl; - @OneToMany(mappedBy = "event") + @OneToMany(mappedBy = "event", fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) private List tags = new ArrayList<>(); @OneToMany(mappedBy = "event") private List comments; @@ -53,6 +56,8 @@ public Event( final LocalDateTime endDate, final String informationUrl ) { + validateStartBeforeOrEqualEndDateTime(startDate, endDate); + this.name = name; this.location = location; this.startDate = startDate; @@ -66,6 +71,16 @@ public Participant addParticipant(final Member member) { return participant; } + public List addAllEventTags(final List tags) { + final List eventTags = tags.stream() + .map(tag -> new EventTag(this, tag)) + .collect(Collectors.toList()); + + this.tags.addAll(eventTags); + + return eventTags; + } + public void validateAlreadyParticipate(final Member member) { if (isAlreadyParticipate(member)) { throw new EventException(EventExceptionType.ALREADY_PARTICIPATED); @@ -77,7 +92,7 @@ private boolean isAlreadyParticipate(final Member member) { .anyMatch(participant -> participant.isSameMember(member)); } - public EventStatus calculateEventStatus(LocalDate now) { + public EventStatus calculateEventStatus(final LocalDate now) { if (now.isBefore(startDate.toLocalDate())) { return EventStatus.UPCOMING; } @@ -86,4 +101,33 @@ public EventStatus calculateEventStatus(LocalDate now) { } return EventStatus.IN_PROGRESS; } + + public Event updateEventContent( + final String name, + final String location, + final LocalDateTime startDate, + final LocalDateTime endDate, + final String informationUrl, + final List tags + ) { + validateStartBeforeOrEqualEndDateTime(startDate, endDate); + + this.name = name; + this.location = location; + this.startDate = startDate; + this.endDate = endDate; + this.informationUrl = informationUrl; + this.tags = new ArrayList<>(); + + addAllEventTags(tags); + + return this; + } + + private void validateStartBeforeOrEqualEndDateTime(final LocalDateTime startDateTime, + final LocalDateTime endDateTime) { + if (startDateTime.isAfter(endDateTime)) { + throw new EventException(EventExceptionType.START_DATE_TIME_AFTER_END_DATE_TIME); + } + } } diff --git a/backend/emm-sale/src/main/java/com/emmsale/event/domain/EventTag.java b/backend/emm-sale/src/main/java/com/emmsale/event/domain/EventTag.java index 572b1ec46..ecb3371e3 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/event/domain/EventTag.java +++ b/backend/emm-sale/src/main/java/com/emmsale/event/domain/EventTag.java @@ -23,11 +23,11 @@ public class EventTag { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "event_id", nullable = false) private Event event; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "tag_id", nullable = false) private Tag tag; - public EventTag(Event event, Tag tag) { + public EventTag(final Event event, final Tag tag) { this.event = event; this.tag = tag; } diff --git a/backend/emm-sale/src/main/java/com/emmsale/event/domain/repository/EventTagRepository.java b/backend/emm-sale/src/main/java/com/emmsale/event/domain/repository/EventTagRepository.java index 5149843e2..8c063f7f7 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/event/domain/repository/EventTagRepository.java +++ b/backend/emm-sale/src/main/java/com/emmsale/event/domain/repository/EventTagRepository.java @@ -9,4 +9,5 @@ public interface EventTagRepository extends JpaRepository { List findEventTagsByTag(Tag tag); + void deleteAllByEventId(Long eventId); } diff --git a/backend/emm-sale/src/main/java/com/emmsale/event/exception/EventExceptionType.java b/backend/emm-sale/src/main/java/com/emmsale/event/exception/EventExceptionType.java index 215098b3a..e2feb57ac 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/event/exception/EventExceptionType.java +++ b/backend/emm-sale/src/main/java/com/emmsale/event/exception/EventExceptionType.java @@ -21,6 +21,14 @@ public enum EventExceptionType implements BaseExceptionType { INVALID_MONTH( HttpStatus.BAD_REQUEST, "월은 1에서 12 사이여야 합니다." + ), + NOT_FOUND_TAG( + HttpStatus.NOT_FOUND, + "해당하는 태그를 찾을 수 없습니다." + ), + START_DATE_TIME_AFTER_END_DATE_TIME( + HttpStatus.BAD_REQUEST, + "행사의 시작 일시가 종료 일시 이후일 수 없습니다." ); private final HttpStatus httpStatus; diff --git a/backend/emm-sale/src/main/java/com/emmsale/notification/application/NotificationQueryService.java b/backend/emm-sale/src/main/java/com/emmsale/notification/application/NotificationQueryService.java index 6c086b71b..a274650e6 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/notification/application/NotificationQueryService.java +++ b/backend/emm-sale/src/main/java/com/emmsale/notification/application/NotificationQueryService.java @@ -1,12 +1,11 @@ package com.emmsale.notification.application; -import static com.emmsale.notification.exception.NotificationExceptionType.*; +import static com.emmsale.notification.exception.NotificationExceptionType.NOT_FOUND_NOTIFICATION; import com.emmsale.notification.application.dto.NotificationResponse; import com.emmsale.notification.domain.Notification; import com.emmsale.notification.domain.NotificationRepository; import com.emmsale.notification.exception.NotificationException; -import com.emmsale.notification.exception.NotificationExceptionType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/emm-sale/src/main/java/com/emmsale/tag/application/dto/TagRequest.java b/backend/emm-sale/src/main/java/com/emmsale/tag/application/dto/TagRequest.java new file mode 100644 index 000000000..64ba6e911 --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/tag/application/dto/TagRequest.java @@ -0,0 +1,16 @@ +package com.emmsale.tag.application.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public class TagRequest { + + private final String name; + + @JsonCreator + public TagRequest(@JsonProperty final String name) { + this.name = name; + } +} diff --git a/backend/emm-sale/src/test/java/com/emmsale/event/api/EventApiTest.java b/backend/emm-sale/src/test/java/com/emmsale/event/api/EventApiTest.java index 290affd32..0dba3f712 100644 --- a/backend/emm-sale/src/test/java/com/emmsale/event/api/EventApiTest.java +++ b/backend/emm-sale/src/test/java/com/emmsale/event/api/EventApiTest.java @@ -10,22 +10,39 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +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.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.emmsale.event.EventFixture; import com.emmsale.event.application.EventService; +import com.emmsale.event.application.dto.EventDetailRequest; import com.emmsale.event.application.dto.EventDetailResponse; import com.emmsale.event.application.dto.EventParticipateRequest; import com.emmsale.event.application.dto.EventResponse; import com.emmsale.event.application.dto.ParticipantResponse; +import com.emmsale.event.domain.Event; +import com.emmsale.event.domain.EventStatus; import com.emmsale.helper.MockMvcTestHelper; +import com.emmsale.tag.TagFixture; +import com.emmsale.tag.application.dto.TagRequest; +import com.emmsale.tag.domain.Tag; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; @@ -33,6 +50,7 @@ import org.springframework.restdocs.payload.RequestFieldsSnippet; import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.restdocs.request.RequestParametersSnippet; +import org.springframework.test.web.servlet.ResultActions; @WebMvcTest(EventApi.class) class EventApiTest extends MockMvcTestHelper { @@ -183,4 +201,318 @@ void findParticipants() throws Exception { .andExpect(status().isOk()) .andDo(document("find-participants", responseFields)); } + + @Test + @DisplayName("이벤트를 성공적으로 업데이트하면 204, NO_CONTENT를 반환한다.") + void updateEventTest() throws Exception { + //given + 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.getStartDate(), + event.getEndDate(), + tags + ); + + final EventDetailResponse response = new EventDetailResponse(eventId, request.getName(), + request.getInformationUrl(), request.getStartDateTime(), request.getEndDateTime(), + request.getLocation(), EventStatus.IN_PROGRESS.getValue(), + tags.stream().map(TagRequest::getName).collect(Collectors.toList())); + + when(eventService.updateEvent(any(), any())).thenReturn(response); + + final RequestFieldsSnippet requestFields = requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("행사(Event) 이름"), + fieldWithPath("location").type(JsonFieldType.STRING).description("행사(Event) 장소"), + fieldWithPath("startDateTime").type(JsonFieldType.STRING).description("행사(Event) 시작일시"), + fieldWithPath("endDateTime").type(JsonFieldType.STRING).description("행사(Event) 종료일시"), + fieldWithPath("informationUrl").type(JsonFieldType.STRING) + .description("행사(Event) 상세 정보 URL"), + fieldWithPath("tags[].name").type(JsonFieldType.STRING).description("연관 태그명") + ); + + final ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("행사(Event) id"), + fieldWithPath("name").type(JsonFieldType.STRING).description("행사(Event) 이름"), + fieldWithPath("informationUrl").type(JsonFieldType.STRING) + .description("행사(Event) 상세 정보 URL"), + fieldWithPath("startDate").type(JsonFieldType.STRING).description("행사(Event) 시작일시"), + fieldWithPath("endDate").type(JsonFieldType.STRING).description("행사(Event) 종료일시"), + fieldWithPath("location").type(JsonFieldType.STRING).description("행사(Event) 장소"), + fieldWithPath("status").type(JsonFieldType.STRING).description("행사(Event) 진행 상태"), + fieldWithPath("tags[]").type(JsonFieldType.ARRAY).description("행사(Event) 연관 태그 목록") + ); + + //when + final ResultActions result = mockMvc.perform(put("/events/" + eventId) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(request))); + + //then + result.andExpect(status().isNoContent()) + .andDo(print()) + .andDo(document("update-event", requestFields, responseFields)); + } + + @Test + @DisplayName("이벤트를 성공적으로 삭제하면 204, NO_CONTENT를 반환한다.") + void deleteEventTest() throws Exception { + //given + final long eventId = 1L; + final Event event = EventFixture.인프콘_2023(); + + final List tags = Stream.of(TagFixture.백엔드(), TagFixture.안드로이드()) + .map(Tag::getName).collect(Collectors.toList()); + + final EventDetailResponse response = new EventDetailResponse(eventId, event.getName(), + event.getInformationUrl(), event.getStartDate(), event.getEndDate(), + event.getLocation(), EventStatus.IN_PROGRESS.getValue(), tags); + + when(eventService.deleteEvent(eventId)).thenReturn(response); + + final ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("행사(Event) id"), + fieldWithPath("name").type(JsonFieldType.STRING).description("행사(Event) 이름"), + fieldWithPath("informationUrl").type(JsonFieldType.STRING) + .description("행사(Event) 상세 정보 URL"), + fieldWithPath("startDate").type(JsonFieldType.STRING).description("행사(Event) 시작일시"), + fieldWithPath("endDate").type(JsonFieldType.STRING).description("행사(Event) 종료일시"), + fieldWithPath("location").type(JsonFieldType.STRING).description("행사(Event) 장소"), + fieldWithPath("status").type(JsonFieldType.STRING).description("행사(Event) 진행 상태"), + fieldWithPath("tags[]").type(JsonFieldType.ARRAY).description("행사(Event) 연관 태그 목록") + ); + + //when + final ResultActions result = mockMvc.perform(delete("/events/" + eventId)); + + //then + result.andExpect(status().isNoContent()) + .andDo(print()) + .andDo(document("delete-event", responseFields)); + } + + @Nested + class AddEvent { + + @Test + @DisplayName("이벤트를 성공적으로 추가하면 201, CREATED 를 반환한다.") + void addEventTest() throws Exception { + //given + 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.getStartDate(), + event.getEndDate(), + tags + ); + + final EventDetailResponse response = new EventDetailResponse(1L, request.getName(), + request.getInformationUrl(), + request.getStartDateTime(), request.getEndDateTime(), request.getLocation(), + EventStatus.IN_PROGRESS.getValue(), + tags.stream().map(TagRequest::getName).collect(Collectors.toList())); + + when(eventService.addEvent(any())).thenReturn(response); + + final RequestFieldsSnippet requestFields = requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("행사(Event) 이름"), + fieldWithPath("location").type(JsonFieldType.STRING).description("행사(Event) 장소"), + fieldWithPath("startDateTime").type(JsonFieldType.STRING).description("행사(Event) 시작일시"), + fieldWithPath("endDateTime").type(JsonFieldType.STRING).description("행사(Event) 종료일시"), + fieldWithPath("informationUrl").type(JsonFieldType.STRING) + .description("행사(Event) 상세 정보 URL"), + fieldWithPath("tags[].name").type(JsonFieldType.STRING).description("연관 태그명") + ); + + final ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("행사(Event) id"), + fieldWithPath("name").type(JsonFieldType.STRING).description("행사(Event) 이름"), + fieldWithPath("informationUrl").type(JsonFieldType.STRING) + .description("행사(Event) 상세 정보 URL"), + fieldWithPath("startDate").type(JsonFieldType.STRING).description("행사(Event) 시작일시"), + fieldWithPath("endDate").type(JsonFieldType.STRING).description("행사(Event) 종료일시"), + fieldWithPath("location").type(JsonFieldType.STRING).description("행사(Event) 장소"), + fieldWithPath("status").type(JsonFieldType.STRING).description("행사(Event) 진행 상태"), + fieldWithPath("tags[]").type(JsonFieldType.ARRAY).description("행사(Event) 연관 태그 목록") + ); + + //when + final ResultActions result = mockMvc.perform(post("/events") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(request))); + + //then + result.andExpect(status().isCreated()) + .andDo(print()) + .andDo(document("add-event", requestFields, responseFields)); + } + + @ParameterizedTest + @NullSource + @EmptySource + @DisplayName("이름에 빈 값이 들어올 경우 400 BAD_REQUEST를 반환한다.") + void addEventWithEmptyNameTest(final String eventName) throws Exception { + //given + 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.getStartDate(), + event.getEndDate(), + tags + ); + + //when + final ResultActions result = mockMvc.perform(post("/events") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(request))); + + //then + result.andExpect(status().isBadRequest()); + } + + @ParameterizedTest + @NullSource + @EmptySource + @DisplayName("장소에 빈 값이 들어올 경우 400 BAD_REQUEST를 반환한다.") + void addEventWithEmptyLocationTest(final String eventLocation) throws Exception { + //given + 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.getStartDate(), + event.getEndDate(), + tags + ); + + //when + final ResultActions result = mockMvc.perform(post("/events") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(request))); + + //then + result.andExpect(status().isBadRequest()); + } + + @ParameterizedTest + @ValueSource(strings = {"httpexample.com", "http:example.com", "http:/example.com", + "httpsexample.com", "https:example.com", "https:/example.com"}) + @NullSource + @DisplayName("상세 URL에 http:// 혹은 https://로 시작하지 않는 값이 들어올 경우 400 BAD_REQUEST를 반환한다.") + void addEventWithInvalidInformationUrlTest(final String informationUrl) throws Exception { + //given + 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(), + informationUrl, + event.getStartDate(), + event.getEndDate(), + tags + ); + + //when + final ResultActions result = mockMvc.perform(post("/events") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(request))); + + //then + result.andExpect(status().isBadRequest()); + } + + @ParameterizedTest + @ValueSource(strings = {"23-01-01T12:00:00", "2023-1-01T12:00:00", "2023-01-1T12:00:00", + "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 { + //given + final Event event = EventFixture.인프콘_2023(); + + final List tags = Stream.of(TagFixture.백엔드(), TagFixture.안드로이드()) + .map(tag -> new TagRequest(tag.getName())) + .collect(Collectors.toList()); + + final String request = "{" + + "\"name\":\"인프콘 2023\"," + + "\"location\":\"코엑스\"," + + "\"informationUrl\":\"https://~~~\"," + + "\"startDateTime\":" + startDateTime + "," + + "\"endDateTime\":\"2023-01-02T12:00:00\"" + + ",\"tags\":[{\"name\":\"백엔드\"},{\"name\":\"안드로이드\"}]" + + "}"; + + //when + final ResultActions result = mockMvc.perform(post("/events") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(request)); + + //then + result.andExpect(status().isBadRequest()); + } + + @ParameterizedTest + @ValueSource(strings = {"23-01-02T12:00:00", "2023-1-02T12:00:00", "2023-01-2T12:00:00", + "2023-01-02T2:00:00", "2023-01-02T12:0:00", "2023-01-02T12:00:0"}) + @NullSource + @DisplayName("종료 일시에 null 혹은 다른 형식의 일시 값이 들어올 경우 400 BAD_REQUEST를 반환한다.") + void addEventWithUnformattedEndDateTimeTest(final String endDateTime) throws Exception { + //given + final Event event = EventFixture.인프콘_2023(); + + final List tags = Stream.of(TagFixture.백엔드(), TagFixture.안드로이드()) + .map(tag -> new TagRequest(tag.getName())) + .collect(Collectors.toList()); + + final String request = "{" + + "\"name\":\"인프콘 2023\"," + + "\"location\":\"코엑스\"," + + "\"informationUrl\":\"https://~~~\"," + + "\"startDateTime\":\"2023-01-01T12:00:00\"" + + "\"endDateTime\":" + endDateTime + "," + + ",\"tags\":[{\"name\":\"백엔드\"},{\"name\":\"안드로이드\"}]" + + "}"; + + //when + final ResultActions result = mockMvc.perform(post("/events") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(request)); + + //then + result.andExpect(status().isBadRequest()); + } + } } 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 a5f15f61b..cb075beb1 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 @@ -13,7 +13,12 @@ import static com.emmsale.tag.TagFixture.프론트엔드; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import com.emmsale.event.application.dto.EventDetailRequest; import com.emmsale.event.application.dto.EventDetailResponse; import com.emmsale.event.application.dto.EventResponse; import com.emmsale.event.application.dto.ParticipantResponse; @@ -26,12 +31,15 @@ import com.emmsale.helper.ServiceIntegrationTestHelper; import com.emmsale.member.domain.Member; import com.emmsale.member.domain.MemberRepository; +import com.emmsale.tag.application.dto.TagRequest; import com.emmsale.tag.domain.Tag; import com.emmsale.tag.domain.TagRepository; import com.emmsale.tag.exception.TagException; import com.emmsale.tag.exception.TagExceptionType; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; +import java.util.stream.Collectors; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -69,17 +77,17 @@ class EventServiceTest extends ServiceIntegrationTestHelper { @BeforeEach void init() { - Tag 백엔드 = tagRepository.save(백엔드()); - Tag 프론트엔드 = tagRepository.save(프론트엔드()); - Tag 안드로이드 = tagRepository.save(안드로이드()); - Tag IOS = tagRepository.save(IOS()); - Tag AI = tagRepository.save(AI()); - - Event 인프콘_2023 = eventRepository.save(인프콘_2023()); - Event AI_컨퍼런스 = eventRepository.save(AI_컨퍼런스()); - Event 모바일_컨퍼런스 = eventRepository.save(모바일_컨퍼런스()); - Event 안드로이드_컨퍼런스 = eventRepository.save(안드로이드_컨퍼런스()); - Event 웹_컨퍼런스 = eventRepository.save(웹_컨퍼런스()); + final Tag 백엔드 = tagRepository.save(백엔드()); + final Tag 프론트엔드 = tagRepository.save(프론트엔드()); + final Tag 안드로이드 = tagRepository.save(안드로이드()); + final Tag IOS = tagRepository.save(IOS()); + final Tag AI = tagRepository.save(AI()); + + final Event 인프콘_2023 = eventRepository.save(인프콘_2023()); + final Event AI_컨퍼런스 = eventRepository.save(AI_컨퍼런스()); + final Event 모바일_컨퍼런스 = eventRepository.save(모바일_컨퍼런스()); + final Event 안드로이드_컨퍼런스 = eventRepository.save(안드로이드_컨퍼런스()); + final Event 웹_컨퍼런스 = eventRepository.save(웹_컨퍼런스()); eventTagRepository.saveAll(List.of( new EventTag(인프콘_2023, 백엔드), new EventTag(인프콘_2023, 프론트엔드), new EventTag(인프콘_2023, 안드로이드), @@ -89,6 +97,32 @@ void init() { ); } + @Test + @DisplayName("event의 id로 참여자 목록을 조회할 수 있다.") + void findParticipants() { + // given + final Event 인프콘 = eventRepository.save(eventFixture()); + final Member 멤버1 = memberRepository.save(new Member(123L, "image1.com")); + final Member 멤버2 = memberRepository.save(new Member(124L, "image2.com")); + + final Long 멤버1_참가자_ID = eventService.participate(인프콘.getId(), 멤버1.getId(), 멤버1); + final Long 멤버2_참가자_ID = eventService.participate(인프콘.getId(), 멤버2.getId(), 멤버2); + + //when + final List actual = eventService.findParticipants(인프콘.getId()); + + final List expected = List.of( + new ParticipantResponse(멤버1_참가자_ID, 멤버1.getId(), 멤버1.getName(), 멤버1.getImageUrl(), + 멤버1.getDescription()), + new ParticipantResponse(멤버2_참가자_ID, 멤버2.getId(), 멤버2.getName(), 멤버2.getImageUrl(), + 멤버2.getDescription()) + ); + //then + assertThat(actual) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrderElementsOf(expected); + } + @Nested @DisplayName("id로 이벤트를 조회할 수 있다.") class findEventTest { @@ -216,7 +250,7 @@ void findEvents_2023_6() { @NullSource @ValueSource(strings = "진행 중") @DisplayName("등록된 행사가 없을 경우, status 옵션이 있든 없든 빈 목록을 반환한다.") - void findEvents_empty(String statusName) { + void findEvents_empty(final String statusName) { // given eventRepository.deleteAll(); @@ -264,7 +298,7 @@ void findEvents_empty_status_filter() { @ParameterizedTest @ValueSource(ints = {2014, 0, -1}) @DisplayName("유효하지 않은 값이 연도 값으로 들어오면 예외를 반환한다.") - void findEvents_year_fail(int year) { + void findEvents_year_fail(final int year) { // given, when final ThrowingCallable actual = () -> eventService.findEvents(TODAY, year, 7, null, null); @@ -277,7 +311,7 @@ void findEvents_year_fail(int year) { @ParameterizedTest @ValueSource(ints = {0, -1, 13, 14}) @DisplayName("유효하지 않은 값이 월 값으로 들어오면 예외를 반환한다.") - void findEvents_month_fail(int month) { + void findEvents_month_fail(final int month) { // given, when final ThrowingCallable actual = () -> eventService.findEvents(TODAY, 2023, month, null, null); @@ -345,36 +379,261 @@ void findEvents_status_filter_fail() { .isInstanceOf(EventException.class) .hasMessage(EventExceptionType.INVALID_STATUS.errorMessage()); } - } - @Test - @DisplayName("event의 id로 참여자 목록을 조회할 수 있다.") - void findParticipants() { - // given - final Event 인프콘 = eventRepository.save(eventFixture()); + @Nested + class AddEvent { + + private final LocalDateTime beforeDateTime = LocalDateTime.now(); + private final LocalDateTime afterDateTime = beforeDateTime.plusDays(1); + + @Test + @DisplayName("이벤트를 성공적으로 저장한다.") + void addEventTest() { + //given + final String eventName = "이름"; + final String eventLocation = "장소"; + final String eventInformationUrl = "https://naver.com"; + final LocalDateTime startDateTime = beforeDateTime; + final LocalDateTime endDateTime = afterDateTime; + final List tagRequests = List.of( + new TagRequest(백엔드().getName()), + new TagRequest(안드로이드().getName()) + ); + + final EventDetailRequest request = new EventDetailRequest(eventName, eventLocation, + eventInformationUrl, startDateTime, endDateTime, tagRequests); + + //when + final EventDetailResponse response = eventService.addEvent(request); + final Event savedEvent = eventRepository.findById(response.getId()).get(); + + //then + assertAll( + () -> assertEquals(eventName, savedEvent.getName()), + () -> assertEquals(eventLocation, savedEvent.getLocation()), + () -> assertEquals(eventInformationUrl, savedEvent.getInformationUrl()), + () -> assertEquals(startDateTime, savedEvent.getStartDate()), + () -> assertEquals(endDateTime, savedEvent.getEndDate()), + () -> assertThat(savedEvent.getTags()).extracting("tag", Tag.class) + .extracting("name", String.class) + .containsAll( + tagRequests.stream() + .map(TagRequest::getName) + .collect(Collectors.toList()) + ) + ); + } + + @Test + @DisplayName("행사 시작 일시가 행사 종료 일시 이후일 경우 EventException이 발생한다.") + void addEventWithStartDateTimeAfterBeforeDateTimeTest() { + //given + final String eventName = "이름"; + final String eventLocation = "장소"; + final String eventInformationUrl = "https://naver.com"; + final LocalDateTime startDateTime = afterDateTime; + final LocalDateTime endDatetime = beforeDateTime; + final List tagRequests = List.of( + new TagRequest(백엔드().getName()), + new TagRequest(안드로이드().getName()) + ); + + final EventDetailRequest request = new EventDetailRequest(eventName, eventLocation, + eventInformationUrl, startDateTime, endDatetime, tagRequests); + + //when & then + final EventException exception = assertThrowsExactly(EventException.class, + () -> eventService.addEvent(request)); + + assertEquals(exception.exceptionType(), + EventExceptionType.START_DATE_TIME_AFTER_END_DATE_TIME); + } + + @Test + @DisplayName("Tag가 존재하지 않을 경우 EventException이 발생한다.") + void addEventWithNotExistTagTest() { + //given + final String eventName = "이름"; + final String eventLocation = "장소"; + final String eventInformationUrl = "https://naver.com"; + final LocalDateTime startDateTime = beforeDateTime; + final LocalDateTime endDatetime = afterDateTime; + final List tagRequests = List.of( + new TagRequest(백엔드().getName()), + new TagRequest(안드로이드().getName()), + new TagRequest("존재하지 않는 태그") + ); + + final EventDetailRequest request = new EventDetailRequest(eventName, eventLocation, + eventInformationUrl, startDateTime, endDatetime, tagRequests); + + //when & then + final EventException exception = assertThrowsExactly(EventException.class, + () -> eventService.addEvent(request)); + + assertEquals(exception.exceptionType(), + EventExceptionType.NOT_FOUND_TAG); + } + } - final Member 저장전_멤버1 = new Member(123L, "image1.com"); - final Member 저장전_멤버2 = new Member(124L, "image2.com"); - 저장전_멤버1.updateName("멈버1"); - 저장전_멤버2.updateName("멈버2"); - final Member 멤버1 = memberRepository.save(저장전_멤버1); - final Member 멤버2 = memberRepository.save(저장전_멤버2); + @Nested + class UpdateEvent { + + private final LocalDateTime beforeDateTime = LocalDateTime.now(); + private final LocalDateTime afterDateTime = beforeDateTime.plusDays(1); + + @Test + @DisplayName("이벤트를 성공적으로 업데이트한다.") + void updateEventTest() { + //given + final String newName = "새로운 이름"; + final String newLocation = "새로운 장소"; + final LocalDateTime newStartDateTime = beforeDateTime; + final LocalDateTime newEndDateTime = afterDateTime; + final String newInformationUrl = "https://새로운-상세-URL.com"; + final List newTagRequests = List.of( + new TagRequest(IOS().getName()), + new TagRequest(AI().getName()) + ); + + final EventDetailRequest updateRequest = new EventDetailRequest(newName, newLocation, + newInformationUrl, newStartDateTime, newEndDateTime, newTagRequests); + + final Event event = eventRepository.save(인프콘_2023()); + final Long eventId = event.getId(); + + //when + eventService.updateEvent(eventId, updateRequest); + final Event updatedEvent = eventRepository.findById(eventId).get(); + + //then + assertAll( + () -> assertEquals(newName, updatedEvent.getName()), + () -> assertEquals(newLocation, updatedEvent.getLocation()), + () -> assertEquals(newStartDateTime, updatedEvent.getStartDate()), + () -> assertEquals(newEndDateTime, updatedEvent.getEndDate()), + () -> assertEquals(newInformationUrl, updatedEvent.getInformationUrl()), + () -> assertThat(updatedEvent.getTags()).extracting("tag", Tag.class) + .extracting("name", String.class) + .containsAll( + newTagRequests.stream() + .map(TagRequest::getName) + .collect(Collectors.toList()) + ) + ); + } + + @Test + @DisplayName("업데이트할 이벤트가 존재하지 않을 경우 EventException이 발생한다.") + void updateEventWithNotExistsEventTest() { + //given + final long notExistsEventId = 0L; + + final String newName = "새로운 이름"; + final String newLocation = "새로운 장소"; + final LocalDateTime newStartDateTime = beforeDateTime; + final LocalDateTime newEndDateTime = afterDateTime; + final String newInformationUrl = "https://새로운-상세-URL.com"; + final List newTagRequests = List.of( + new TagRequest(IOS().getName()), + new TagRequest(AI().getName()) + ); + + final EventDetailRequest updateRequest = new EventDetailRequest(newName, newLocation, + newInformationUrl, newStartDateTime, newEndDateTime, newTagRequests); + + //when & then + final EventException exception = assertThrowsExactly(EventException.class, + () -> eventService.updateEvent(notExistsEventId, updateRequest)); + + assertEquals(exception.exceptionType(), EventExceptionType.NOT_FOUND_EVENT); + } + + @Test + @DisplayName("행사 시작 일시가 행사 종료 일시 이후일 경우 EventException이 발생한다.") + void updateEventWithStartDateTimeAfterBeforeDateTimeTest() { + //given + final String newName = "새로운 이름"; + final String newLocation = "새로운 장소"; + final LocalDateTime newStartDateTime = afterDateTime; + final LocalDateTime newEndDateTime = beforeDateTime; + final String newInformationUrl = "https://새로운-상세-URL.com"; + final List newTagRequests = List.of( + new TagRequest(IOS().getName()), + new TagRequest(AI().getName()) + ); + + final EventDetailRequest updateRequest = new EventDetailRequest(newName, newLocation, + newInformationUrl, newStartDateTime, newEndDateTime, newTagRequests); + + final Event event = eventRepository.save(인프콘_2023()); + final Long eventId = event.getId(); + + //when & then + final EventException exception = assertThrowsExactly(EventException.class, + () -> eventService.updateEvent(eventId, updateRequest)); + + assertEquals(exception.exceptionType(), + EventExceptionType.START_DATE_TIME_AFTER_END_DATE_TIME); + } + + @Test + @DisplayName("Tag가 존재하지 않을 경우 EventException이 발생한다.") + void updateEventWithNotExistTagTest() { + //given + final String newName = "새로운 이름"; + final String newLocation = "새로운 장소"; + final LocalDateTime newStartDateTime = beforeDateTime; + final LocalDateTime newEndDateTime = afterDateTime; + final String newInformationUrl = "https://새로운-상세-URL.com"; + final List newTagRequests = List.of( + new TagRequest("존재하지 않는 태그") + ); + + final EventDetailRequest updateRequest = new EventDetailRequest(newName, newLocation, + newInformationUrl, newStartDateTime, newEndDateTime, newTagRequests); + + final Event event = eventRepository.save(인프콘_2023()); + final Long eventId = event.getId(); + + //when & then + final EventException exception = assertThrowsExactly(EventException.class, + () -> eventService.updateEvent(eventId, updateRequest)); + + assertEquals(exception.exceptionType(), EventExceptionType.NOT_FOUND_TAG); + } + } - final Long 멤버1_참가자_ID = eventService.participate(인프콘.getId(), 멤버1.getId(), 멤버1); - final Long 멤버2_참가자_ID = eventService.participate(인프콘.getId(), 멤버2.getId(), 멤버2); + @Nested + class DeleteEvent { - //when - final List actual = eventService.findParticipants(인프콘.getId()); + @Test + @DisplayName("이벤트를 성공적으로 삭제한다.") + void deleteEventTest() { + //given + final Event event = eventRepository.save(인프콘_2023()); + final Long eventId = event.getId(); + + //when + eventService.deleteEvent(eventId); + + //then + assertFalse(eventRepository.findById(eventId).isPresent()); + } + + @Test + @DisplayName("삭제할 이벤트가 존재하지 않을 경우 EventException이 발생한다.") + void deleteEventWithNotExistsEventTest() { + //given + final long notExistsEventId = 0L; + + //when & then + final EventException exception = assertThrowsExactly(EventException.class, + () -> eventService.deleteEvent(notExistsEventId)); + + assertEquals(exception.exceptionType(), EventExceptionType.NOT_FOUND_EVENT); + } + } - final List expected = List.of( - new ParticipantResponse(멤버1_참가자_ID, 멤버1.getId(), 멤버1.getName(), 멤버1.getImageUrl(), - 멤버1.getDescription()), - new ParticipantResponse(멤버2_참가자_ID, 멤버2.getId(), 멤버2.getName(), 멤버2.getImageUrl(), - 멤버2.getDescription()) - ); - //then - assertThat(actual) - .usingRecursiveFieldByFieldElementComparator() - .containsExactlyInAnyOrderElementsOf(expected); } } diff --git a/backend/emm-sale/src/test/java/com/emmsale/event/domain/EventTest.java b/backend/emm-sale/src/test/java/com/emmsale/event/domain/EventTest.java index be01cab0c..c230ae1d9 100644 --- a/backend/emm-sale/src/test/java/com/emmsale/event/domain/EventTest.java +++ b/backend/emm-sale/src/test/java/com/emmsale/event/domain/EventTest.java @@ -2,11 +2,17 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; import com.emmsale.event.EventFixture; import com.emmsale.event.exception.EventException; import com.emmsale.event.exception.EventExceptionType; import com.emmsale.member.domain.Member; +import com.emmsale.tag.TagFixture; +import com.emmsale.tag.domain.Tag; +import java.time.LocalDateTime; import java.time.LocalDate; import java.util.List; import java.util.stream.Collectors; @@ -31,6 +37,78 @@ void calculateEventStatus(LocalDate input, EventStatus expected) { } + @Test + @DisplayName("Event 생성시 startDate가 endDate 이후일 경우 EventException이 발생한다.") + void newEventWithStartDateAfterEndDateTest() { + //given + final String name = "이름"; + final String location = "장소"; + final String url = "https://information-url.com"; + final LocalDateTime beforeDateTime = LocalDateTime.now(); + final LocalDateTime afterDateTime = beforeDateTime.plusDays(1); + + //when & then + final EventException exception = assertThrowsExactly(EventException.class, + () -> new Event(name, location, afterDateTime, beforeDateTime, url)); + + assertEquals(EventExceptionType.START_DATE_TIME_AFTER_END_DATE_TIME, exception.exceptionType()); + } + + @Test + @DisplayName("Event의 name, location, startDate, endDate, informationUrl, tags를 업데이트할 수 있다.") + void updateEventContentTest() { + //given + final String newName = "새로운 이름"; + final String newLocation = "새로운 장소"; + final LocalDateTime newStartDateTime = LocalDateTime.now(); + final LocalDateTime newEndDateTime = newStartDateTime.plusDays(1); + final String newInformationUrl = "https://새로운-상세-URL.com"; + final List newTags = List.of(TagFixture.IOS(), TagFixture.AI()); + + final Event event = EventFixture.인프콘_2023(); + + //when + final Event updatedEvent = event.updateEventContent( + newName, + newLocation, + newStartDateTime, + newEndDateTime, + newInformationUrl, + newTags + ); + + //then + assertAll( + () -> assertEquals(newName, updatedEvent.getName()), + () -> assertEquals(newLocation, updatedEvent.getLocation()), + () -> assertEquals(newStartDateTime, updatedEvent.getStartDate()), + () -> assertEquals(newEndDateTime, updatedEvent.getEndDate()), + () -> assertEquals(newInformationUrl, updatedEvent.getInformationUrl()), + () -> assertEquals(newTags.size(), event.getTags().size()) + ); + } + + @Test + @DisplayName("eventContent 업데이트시 startDate가 endDate 이후일 경우 EventException이 발생한다.") + void updateEventContentWithStartDateAfterEndDateTest() { + //given + final String newName = "새로운 이름"; + final String newLocation = "새로운 장소"; + final LocalDateTime beforeDateTime = LocalDateTime.now(); + final LocalDateTime afterDateTime = beforeDateTime.plusDays(1); + final String newInformationUrl = "https://새로운-상세-URL.com"; + final List newTags = List.of(TagFixture.IOS(), TagFixture.AI()); + + final Event event = EventFixture.인프콘_2023(); + + //when & then + final EventException exception = assertThrowsExactly(EventException.class, + () -> event.updateEventContent(newName, newLocation, afterDateTime, beforeDateTime, + newInformationUrl, newTags)); + + assertEquals(EventExceptionType.START_DATE_TIME_AFTER_END_DATE_TIME, exception.exceptionType()); + } + @Nested class addParticipant { diff --git a/backend/emm-sale/src/test/java/com/emmsale/event/domain/repository/EventRepositoryTest.java b/backend/emm-sale/src/test/java/com/emmsale/event/domain/repository/EventRepositoryTest.java index 5346c3bfa..6284c778b 100644 --- a/backend/emm-sale/src/test/java/com/emmsale/event/domain/repository/EventRepositoryTest.java +++ b/backend/emm-sale/src/test/java/com/emmsale/event/domain/repository/EventRepositoryTest.java @@ -4,7 +4,6 @@ import com.emmsale.event.EventFixture; import com.emmsale.event.domain.Event; -import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired;