diff --git a/README.md b/README.md new file mode 100644 index 000000000..c6e91be22 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# 방탈출 API + +| Method | Endpoint | Description | +|--------|----------------------|--------------| +| GET | `/admin` | 어드민 페이지 요청 | +| GET | `/admin/reservation` | 예약 관리 페이지 요청 | +| GET | `/reservations` | 예약 정보 | +| POST | `/reservations` | 예약 추가 | +| DELETE | `/reservations/{id}` | 예약 취소 | + +# 방탈출 예약 도메인 명세 + +## Name + +- [x] 예약자 이름은 null이거나 빈 문자열일 수 없다. +- [x] 예약자 이름은 1~5사이의 길이를 가져야 한다. + +## ReservationTime + +- [x] 예약하려는 시간에 다른 예약이 존재한다면 예약이 불가능하다. +- [x] 예약 시간은 예약 시작 시간으로부터 한 시간의 길이를 가진다. diff --git a/build.gradle b/build.gradle index fdb3c9f06..12dc09e67 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ dependencies { runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' + testImplementation 'org.mockito:mockito-core:3.6.28' } test { diff --git a/src/main/java/roomescape/RoomescapeApplication.java b/src/main/java/roomescape/RoomescapeApplication.java index 702706791..2ca0f743f 100644 --- a/src/main/java/roomescape/RoomescapeApplication.java +++ b/src/main/java/roomescape/RoomescapeApplication.java @@ -8,5 +8,4 @@ public class RoomescapeApplication { public static void main(String[] args) { SpringApplication.run(RoomescapeApplication.class, args); } - } diff --git a/src/main/java/roomescape/controller/AdminViewController.java b/src/main/java/roomescape/controller/AdminViewController.java new file mode 100644 index 000000000..7efcc5654 --- /dev/null +++ b/src/main/java/roomescape/controller/AdminViewController.java @@ -0,0 +1,19 @@ +package roomescape.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@RequestMapping("/admin") +@Controller +public class AdminViewController { + @GetMapping() + public String showAdminMainPage() { + return "admin/index"; + } + + @GetMapping("/reservation") + public String showAdminReservationPage() { + return "admin/reservation-legacy"; + } +} diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java new file mode 100644 index 000000000..ff9c82c25 --- /dev/null +++ b/src/main/java/roomescape/controller/ReservationController.java @@ -0,0 +1,46 @@ +package roomescape.controller; + +import java.util.List; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import roomescape.controller.dto.ReservationRequest; +import roomescape.controller.dto.ReservationResponse; +import roomescape.entity.Reservation; +import roomescape.service.ReservationService; + +@RequestMapping("/reservations") +@RestController +public class ReservationController { + private final ReservationService reservationService; + + public ReservationController(ReservationService reservationService) { + this.reservationService = reservationService; + } + + @GetMapping() + public ResponseEntity> readAllReservations() { + List reservations = reservationService.readAll() + .stream() + .map(ReservationResponse::from) + .toList(); + return ResponseEntity.ok().body(reservations); + } + + @PostMapping() + public ResponseEntity createReservation(@RequestBody ReservationRequest reservationRequest) { + Reservation savedReservation = reservationService.saveReservation(reservationRequest.toEntity()); + return ResponseEntity.ok().body(ReservationResponse.from(savedReservation)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteReservationById(@PathVariable("id") long id) { + reservationService.deleteReservation(id); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/roomescape/controller/dto/ReservationRequest.java b/src/main/java/roomescape/controller/dto/ReservationRequest.java new file mode 100644 index 000000000..636966108 --- /dev/null +++ b/src/main/java/roomescape/controller/dto/ReservationRequest.java @@ -0,0 +1,36 @@ +package roomescape.controller.dto; + +import java.time.LocalDate; +import java.time.LocalTime; +import roomescape.entity.Reservation; + +public class ReservationRequest { + private String name; + private LocalDate date; + private LocalTime time; + + public ReservationRequest() { + } + + public ReservationRequest(String name, LocalDate date, LocalTime time) { + this.name = name; + this.date = date; + this.time = time; + } + + public Reservation toEntity() { + return new Reservation(name, date, time); + } + + public String getName() { + return name; + } + + public LocalDate getDate() { + return date; + } + + public LocalTime getTime() { + return time; + } +} diff --git a/src/main/java/roomescape/controller/dto/ReservationResponse.java b/src/main/java/roomescape/controller/dto/ReservationResponse.java new file mode 100644 index 000000000..0dd6821de --- /dev/null +++ b/src/main/java/roomescape/controller/dto/ReservationResponse.java @@ -0,0 +1,43 @@ +package roomescape.controller.dto; + +import java.time.LocalDate; +import java.time.LocalTime; +import roomescape.entity.Reservation; + +public class ReservationResponse { + private final Long id; + private final String name; + private final LocalDate date; + private final LocalTime time; + + public ReservationResponse(long id, String name, LocalDate date, LocalTime time) { + this.id = id; + this.name = name; + this.date = date; + this.time = time; + } + + public static ReservationResponse from(Reservation reservation) { + return new ReservationResponse( + reservation.getId(), + reservation.getName(), + reservation.getStartDate(), + reservation.getStartTime()); + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public LocalDate getDate() { + return date; + } + + public LocalTime getTime() { + return time; + } +} diff --git a/src/main/java/roomescape/entity/Name.java b/src/main/java/roomescape/entity/Name.java new file mode 100644 index 000000000..8ec3f38b6 --- /dev/null +++ b/src/main/java/roomescape/entity/Name.java @@ -0,0 +1,35 @@ +package roomescape.entity; + +public class Name { + private static final int MIN_LENGTH = 1; + private static final int MAX_LENGTH = 5; + + private final String name; + + public Name(String name) { + validate(name); + this.name = name; + } + + private void validate(String name) { + validateNonNull(name); + validateLength(name); + } + + private void validateNonNull(String name) { + if (name == null) { + throw new NullPointerException("예약자 이름은 Null이 될 수 없습니다"); + } + } + + private void validateLength(String name) { + if (MAX_LENGTH < name.length() || name.length() < MIN_LENGTH) { + throw new IllegalArgumentException( + "예약자 이름은 " + MIN_LENGTH + "자 이상, " + MAX_LENGTH + "자 미만이어야 합니다: " + name); + } + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/roomescape/entity/Reservation.java b/src/main/java/roomescape/entity/Reservation.java new file mode 100644 index 000000000..3735a116d --- /dev/null +++ b/src/main/java/roomescape/entity/Reservation.java @@ -0,0 +1,45 @@ +package roomescape.entity; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public class Reservation { + private final Long id; + private final Name name; + private final ReservationTime time; + + public Reservation(Long id, String name, LocalDate reservationDate, LocalTime reservationStartTime) { + this.id = id; + this.name = new Name(name); + this.time = new ReservationTime(LocalDateTime.of(reservationDate, reservationStartTime)); + } + + public Reservation(String name, LocalDate reservationDate, LocalTime reservationStartTime) { + this(null, name, reservationDate, reservationStartTime); + } + + public long getId() { + return id; + } + + public String getName() { + return name.getName(); + } + + public LocalDateTime getStartDateTime() { + return time.getStartDateTime(); + } + + public LocalDateTime getEndDateTime() { + return time.getEndDateTime(); + } + + public LocalDate getStartDate() { + return time.getStartDate(); + } + + public LocalTime getStartTime() { + return time.getStart(); + } +} diff --git a/src/main/java/roomescape/entity/ReservationTime.java b/src/main/java/roomescape/entity/ReservationTime.java new file mode 100644 index 000000000..76a3e49b2 --- /dev/null +++ b/src/main/java/roomescape/entity/ReservationTime.java @@ -0,0 +1,38 @@ +package roomescape.entity; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public class ReservationTime { + public static final int RESERVATION_DURATION_HOUR = 1; + + private final LocalDateTime start; + + public ReservationTime(LocalDateTime start) { + validateNonNull(start); + this.start = start; + } + + private void validateNonNull(LocalDateTime startTime) { + if (startTime == null) { + throw new NullPointerException("예약 시간은 Null이 될 수 없습니다"); + } + } + + public LocalDateTime getStartDateTime() { + return start; + } + + public LocalDateTime getEndDateTime() { + return start.plusHours(RESERVATION_DURATION_HOUR); + } + + public LocalDate getStartDate() { + return start.toLocalDate(); + } + + public LocalTime getStart() { + return start.toLocalTime(); + } +} diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java new file mode 100644 index 000000000..43f1c669e --- /dev/null +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -0,0 +1,90 @@ +package roomescape.repository; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.sql.Time; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Optional; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import roomescape.entity.Reservation; +import roomescape.entity.ReservationTime; + +@Repository +public class ReservationRepository { + private final JdbcTemplate jdbcTemplate; + + public ReservationRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List readAll() { + String sql = "select * from reservation"; + return jdbcTemplate.query(sql, reservationRowMapper()); + } + + public Reservation save(Reservation reservation) { + String sql = "insert into reservation (name, date, time) values(?, ?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setString(1, reservation.getName()); + ps.setDate(2, Date.valueOf(reservation.getStartDate())); + ps.setTime(3, Time.valueOf(reservation.getStartTime())); + return ps; + }, keyHolder); + long savedId = keyHolder.getKey().longValue(); + return new Reservation(savedId, reservation.getName(), reservation.getStartDate(), reservation.getStartTime()); + } + + public Optional findById(long id) { + String sql = "select * from reservation where id=?"; + try { + return Optional.of(jdbcTemplate.queryForObject(sql, new Object[]{id}, reservationRowMapper())); + } catch (EmptyResultDataAccessException ex) { + return Optional.empty(); + } + } + + public void deleteById(long id) { + String sql = "delete from reservation where id=?"; + jdbcTemplate.update(sql, id); + } + + public boolean isAnyReservationConflictWith(Reservation reservation) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String startDateTime = reservation.getStartDateTime().format(formatter); + String endDateTime = reservation.getEndDateTime().format(formatter); + + String sql = "select exists (" + + " select 1 " + + " from reservation " + + " where ? between (date || ' ' || time) and dateadd('HOUR', ?, (date || ' ' || time)) " + + " or ? between (date || ' ' || time) and dateadd('HOUR', ?, (date || ' ' || time)) " + + ") as exists_overlap;"; + + boolean conflict = jdbcTemplate.queryForObject(sql, Boolean.class, endDateTime, + ReservationTime.RESERVATION_DURATION_HOUR, startDateTime, ReservationTime.RESERVATION_DURATION_HOUR); + return conflict; + } + + private RowMapper reservationRowMapper() { + return (rs, rowNum) -> { + long id = rs.getLong("id"); + String name = rs.getString("name"); + LocalDate date = rs.getDate("date").toLocalDate(); + LocalTime time = rs.getTime("time").toLocalTime(); + + return new Reservation(id, name, date, time); + }; + } +} diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java new file mode 100644 index 000000000..292b3bb6f --- /dev/null +++ b/src/main/java/roomescape/service/ReservationService.java @@ -0,0 +1,31 @@ +package roomescape.service; + +import java.util.List; +import org.springframework.stereotype.Service; +import roomescape.entity.Reservation; +import roomescape.repository.ReservationRepository; + +@Service +public class ReservationService { + private final ReservationRepository reservationRepository; + + public ReservationService(ReservationRepository reservationRepository) { + this.reservationRepository = reservationRepository; + } + + public List readAll() { + return reservationRepository.readAll(); + } + + public Reservation saveReservation(Reservation reservation) { + if (reservationRepository.isAnyReservationConflictWith(reservation)) { + throw new IllegalStateException( + "해당 예약 시간에 예약이 이미 존재합니다: " + reservation.getStartDate() + " " + reservation.getStartTime()); + } + return reservationRepository.save(reservation); + } + + public void deleteReservation(long id) { + reservationRepository.deleteById(id); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..ef92e87c8 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,6 @@ +spring: + h2: + console: + enabled: true + datasource: + url: jdbc:h2:mem:database diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..c36eb3485 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS reservation +( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + date VARCHAR(255) NOT NULL, + time VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); diff --git a/src/main/resources/templates/admin/index.html b/src/main/resources/templates/admin/index.html index 417102cf1..44716c9f9 100644 --- a/src/main/resources/templates/admin/index.html +++ b/src/main/resources/templates/admin/index.html @@ -1,38 +1,38 @@ - - - 방탈출 어드민 - - - + + + 방탈출 어드민 + + +
-

방탈출 어드민

+

방탈출 어드민

diff --git a/src/main/resources/templates/admin/reservation-legacy.html b/src/main/resources/templates/admin/reservation-legacy.html index 320ef3c70..44ccad54e 100644 --- a/src/main/resources/templates/admin/reservation-legacy.html +++ b/src/main/resources/templates/admin/reservation-legacy.html @@ -1,55 +1,55 @@ - - - 방탈출 어드민 - - - + + + 방탈출 어드민 + + +
-

방탈출 예약 페이지

-
- -
-
- - - - - - - - - - - - -
예약번호예약자날짜시간
+

방탈출 예약 페이지

+
+ +
+
+ + + + + + + + + + + + +
예약번호예약자날짜시간
diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java deleted file mode 100644 index 6f7b19791..000000000 --- a/src/test/java/roomescape/MissionStepTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package roomescape; - -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -public class MissionStepTest { - - @Test - void 일단계() { - RestAssured.given().log().all() - .when().get("/") - .then().log().all() - .statusCode(200); - } - -} diff --git a/src/test/java/roomescape/controller/AdminViewControllerTest.java b/src/test/java/roomescape/controller/AdminViewControllerTest.java new file mode 100644 index 000000000..5c1db75a0 --- /dev/null +++ b/src/test/java/roomescape/controller/AdminViewControllerTest.java @@ -0,0 +1,28 @@ +package roomescape.controller; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class AdminViewControllerTest { + @DisplayName("어드민 메인 페이지 요청 테스트") + @Test + void adminMainPageTest() { + RestAssured.given().log().all() + .when().get("/admin") + .then().log().all() + .statusCode(200); + } + + @DisplayName("어드민 예약 페이지 요청 테스트") + @Test + void adminReservationPageTest() { + RestAssured.given().log().all() + .when().get("/admin/reservation") + .then().log().all() + .statusCode(200); + } + +} diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java new file mode 100644 index 000000000..8ecfd735c --- /dev/null +++ b/src/test/java/roomescape/controller/ReservationControllerTest.java @@ -0,0 +1,59 @@ +package roomescape.controller; + +import static org.hamcrest.Matchers.is; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class ReservationControllerTest { + @DisplayName("전체 예약 정보 요청 테스트") + @Test + void reservationsTest() { + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(0)); + } + + @DisplayName("예약 생성, 삭제 테스트") + @Test + void reservationCreationAndDeleteTest() { + Map params = new HashMap<>(); + params.put("name", "브라운"); + params.put("date", "2023-08-05"); + params.put("time", "15:40"); + + int savedId = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(200) + .extract().path("id"); + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(1)); + + RestAssured.given().log().all() + .when().delete("/reservations/" + savedId) + .then().log().all() + .statusCode(200); + + RestAssured.given().log().all() + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(0)); + } + +} diff --git a/src/test/java/roomescape/entity/NameTest.java b/src/test/java/roomescape/entity/NameTest.java new file mode 100644 index 000000000..fb9bc7414 --- /dev/null +++ b/src/test/java/roomescape/entity/NameTest.java @@ -0,0 +1,34 @@ +package roomescape.entity; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class NameTest { + @DisplayName("예약자 이름이 null인 경우 생성 시 예외가 발생한다") + @Test + void nullNameCreationTest() { + assertThatThrownBy(() -> new Name(null)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("예약자 이름의 길이가 범위에 맞지 않는 경우 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"", "123456"}) + void invalidLengthNameCreationTest(String name) { + assertThatThrownBy(() -> new Name(name)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("조건에 맞는 이름을 생성할 경우 예외가 발생하지 않는다") + @ParameterizedTest + @ValueSource(strings = {"1", "12", "123", "1234", "12345"}) + void validNameCreationTest(String name) { + assertThatCode(() -> new Name(name)) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/roomescape/entity/ReservationTest.java b/src/test/java/roomescape/entity/ReservationTest.java new file mode 100644 index 000000000..a41f3c7d1 --- /dev/null +++ b/src/test/java/roomescape/entity/ReservationTest.java @@ -0,0 +1,19 @@ +package roomescape.entity; + +import static org.assertj.core.api.Assertions.assertThat; +import static roomescape.entity.ReservationTime.RESERVATION_DURATION_HOUR; + +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ReservationTest { + @DisplayName("예약이 끝나는 시간을 계산할 수 있다") + @Test + void calculateReservationEndTimeTest() { + LocalDateTime startTime = LocalDateTime.of(2024, 4, 20, 12, 30); + Reservation reservation = new Reservation("리비", startTime.toLocalDate(), startTime.toLocalTime()); + + assertThat(reservation.getEndDateTime()).isEqualTo(startTime.plusHours(RESERVATION_DURATION_HOUR)); + } +} diff --git a/src/test/java/roomescape/entity/ReservationTimeTest.java b/src/test/java/roomescape/entity/ReservationTimeTest.java new file mode 100644 index 000000000..d8e0d904f --- /dev/null +++ b/src/test/java/roomescape/entity/ReservationTimeTest.java @@ -0,0 +1,27 @@ +package roomescape.entity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static roomescape.entity.ReservationTime.RESERVATION_DURATION_HOUR; + +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ReservationTimeTest { + @DisplayName("예약 시간이 null일 경우 생성에 실패한다") + @Test + void reservationTimeCreationTestWithNull() { + assertThatThrownBy(() -> new ReservationTime(null)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("예약이 끝나는 시간은 시작 시간으로부터 지정된 기간 뒤 까지이다") + @Test + void reservationDurationTest() { + LocalDateTime startTime = LocalDateTime.of(2024, 4, 20, 12, 30); + ReservationTime reservationTime = new ReservationTime(startTime); + + assertThat(reservationTime.getEndDateTime()).isEqualTo(startTime.plusHours(RESERVATION_DURATION_HOUR)); + } +} diff --git a/src/test/java/roomescape/repository/H2DatabaseTest.java b/src/test/java/roomescape/repository/H2DatabaseTest.java new file mode 100644 index 000000000..72a9b2251 --- /dev/null +++ b/src/test/java/roomescape/repository/H2DatabaseTest.java @@ -0,0 +1,47 @@ +package roomescape.repository; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.sql.Connection; +import java.sql.SQLException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class H2DatabaseTest { + @Autowired + private JdbcTemplate jdbcTemplate; + + @DisplayName("커넥션을 얻어올 수 있다") + @Test + void connectionTest() { + try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { + assertThat(connection).isNotNull(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @DisplayName("데이터 베이스의 논리적 구분 이름은 'database'이다") + @Test + void catalogTest() { + try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { + assertThat(connection.getCatalog()).isEqualTo("DATABASE"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @DisplayName("데이터 베이스에 reservation 테이블이 존재한다") + @Test + void tablesTest() { + try (Connection connection = jdbcTemplate.getDataSource().getConnection()) { + assertThat(connection.getMetaData().getTables(null, null, "RESERVATION", null).next()).isTrue(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/roomescape/repository/ReservationRepositoryTest.java b/src/test/java/roomescape/repository/ReservationRepositoryTest.java new file mode 100644 index 000000000..6abdf8363 --- /dev/null +++ b/src/test/java/roomescape/repository/ReservationRepositoryTest.java @@ -0,0 +1,82 @@ +package roomescape.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; +import roomescape.entity.Reservation; + +@SpringBootTest +@Transactional +@Rollback +class ReservationRepositoryTest { + + @Autowired + private ReservationRepository reservationRepository; + + @DisplayName("전체 예약을 조회할 수 있다") + @Test + void readAllTest() { + Reservation reservation1 = new Reservation(null, "리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + Reservation reservation2 = new Reservation(null, "웨지", LocalDate.of(2024, 4, 20), LocalTime.of(4, 57)); + + reservationRepository.save(reservation1); + reservationRepository.save(reservation2); + + List reservations = reservationRepository.readAll(); + + assertThat(reservationRepository.readAll()) + .extracting("name") + .containsExactly("리비", "웨지"); + } + + @DisplayName("예약 단건을 저장할 수 있다") + @Test + void saveTest() { + Reservation reservation = new Reservation("폭포", LocalDate.of(2024, 5, 20), LocalTime.of(3, 57)); + Reservation saved = reservationRepository.save(reservation); + Optional found = reservationRepository.findById(saved.getId()); + + assertThat(found).isPresent(); + } + + @DisplayName("예약 단건을 조회할 수 있다") + @Test + void findByIdTest() { + Reservation reservation = new Reservation(null, "리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + + Reservation saved = reservationRepository.save(reservation); + + assertThat(reservationRepository.findById(saved.getId())).isPresent(); + } + + @DisplayName("예약 단건을 삭제할 수 있다") + @Test + void deleteByIdTest() { + Reservation reservation = new Reservation(null, "리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + + Reservation saved = reservationRepository.save(reservation); + reservationRepository.deleteById(saved.getId()); + + assertThat(reservationRepository.findById(1L)).isEmpty(); + } + + @DisplayName("특정 예약이 저장된 예약들과 시간이 겹치는 경우가 있는지 확인할 수 있다") + @Test + void isAnyReservationConflictWithTest() { + Reservation reservation = new Reservation(null, "리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + + reservationRepository.save(reservation); + Reservation conflictReservation = new Reservation(3L, "폭포", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + + assertThat(reservationRepository.isAnyReservationConflictWith(conflictReservation)).isTrue(); + } +} diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java new file mode 100644 index 000000000..cda92fa7d --- /dev/null +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -0,0 +1,42 @@ +package roomescape.service; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDate; +import java.time.LocalTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; +import roomescape.entity.Reservation; + +@SpringBootTest +@Transactional +@Rollback +class ReservationServiceTest { + @Autowired + private ReservationService reservationService; + + @DisplayName("시간이 겹치는 예약이 존재하지 않는 경우 예약에 성공한다") + @Test + void reservationSaveSuccessTest() { + Reservation reservation = new Reservation("리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + + assertThatCode(() -> reservationService.saveReservation(reservation)) + .doesNotThrowAnyException(); + } + + @DisplayName("시간이 겹치는 예약이 존재할 경우 예약에 실패한다") + @Test + void reservationSaveFailByTimeConflictTest() { + Reservation reservation = new Reservation("리비", LocalDate.of(2024, 4, 20), LocalTime.of(3, 57)); + Reservation conflictReservation = new Reservation("웨지", LocalDate.of(2024, 4, 20), LocalTime.of(3, 30)); + reservationService.saveReservation(reservation); + + assertThatThrownBy(() -> reservationService.saveReservation(conflictReservation)) + .isInstanceOf(IllegalStateException.class); + } +}