diff --git a/docs/README.md b/docs/README.md index 787d32ab7e..4d4d212fb2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,4 +23,42 @@ - 인수 조건을 검증하는 인수 테스트 작성 - 인수 테스트를 충족하는 기능 구현 - 인수 테스트의 결과가 다른 인수 테스트에 영향을 끼치지 않도록 격리 -- 인수 테스트의 재사용성, 가독성, 빠른 테스트 파악을 위한 리팩토링 \ No newline at end of file +- 인수 테스트의 재사용성, 가독성, 빠른 테스트 파악을 위한 리팩토링 + +## Step3. 구간 추가 기능 + +### 기능 요구사항 +- 지하철 구간 추가 도메인 기능 구현 + - [X] 새 구간 등록 + - [X] 역 사이에 새로운 역 등록 + - [X] 기능 구현 + - [X] 테스트 코드 작성 + - [X] 새로운 역을 상행 종점으로 등록 + - [X] 기능 구현 + - [X] 테스트 코드 작성 + - [X] 새로운 역을 하행 종점으로 등록 + - [X] 기능 구현 + - [X] 테스트 코드 작성 + - [X] 새 구간 등록 불가능 + - [X] 역 사이에 새로운 역을 등록하는 경우 기존 역 사이 길이보다 크거나 같으면 등록 불가 + - [X] 기능 구현 + - [X] 테스트 코드 작성 + - [X] 상행역과 하행역이 이미 노선에 모두 등록되어 있다면 추가 불가능 + - [X] 기능 구현 + - [X] 테스트 코드 작성 + - [X] 상행역과 하행역 둘 중 하나도 포함되어 있지 않다면 추가 불가능 + - [X] 기능 구현 + - [X] 테스트 코드 작성 +- [X] 요구사항을 정의한 인수 조건 조출 +- [X] 인수 조건을 검증하는 인수 테스트 작성 +- [X] 예외 케이스에 대한 검증 포함 + +### 프로그래밍 요구사항 +- 인수 테스트 주도 개발 프로세스에 맞춰서 기능 구현 + - 요구사항 설명을 참고하여 인수 조건 정의 + - 인수 조건을 검증하는 인수 테스트 작성 + - 인수 테스트를 충족하는 기능 구현 +- 인수 조건은 인수 테스트 메서드 상단에 주석으로 작성 + - 뼈대 코드의 인수 테스트 참고 +- 인수 테스트의 결과가 다른 인수 테스트에 영향을 끼치지 않도록 격리 +- 인수 테스트의 재사용성과, 가독성, 그리고 빠른 테스트 의도 파악을 위해 리팩토링 \ No newline at end of file diff --git a/src/main/java/nextstep/subway/line/CreateLineDto.java b/src/main/java/nextstep/subway/line/CreateLineRequest.java similarity index 84% rename from src/main/java/nextstep/subway/line/CreateLineDto.java rename to src/main/java/nextstep/subway/line/CreateLineRequest.java index 2d96c36d7d..00acfd5a44 100644 --- a/src/main/java/nextstep/subway/line/CreateLineDto.java +++ b/src/main/java/nextstep/subway/line/CreateLineRequest.java @@ -1,6 +1,6 @@ package nextstep.subway.line; -class CreateLineDto { +class CreateLineRequest { private String name; @@ -12,7 +12,7 @@ class CreateLineDto { private long distance; - public CreateLineDto(String name, String color, long upStationId, long downStationId, long distance) { + public CreateLineRequest(String name, String color, long upStationId, long downStationId, long distance) { this.name = name; this.color = color; this.upStationId = upStationId; diff --git a/src/main/java/nextstep/subway/line/Line.java b/src/main/java/nextstep/subway/line/Line.java index 98ca89bf11..42d3bdc400 100644 --- a/src/main/java/nextstep/subway/line/Line.java +++ b/src/main/java/nextstep/subway/line/Line.java @@ -54,7 +54,7 @@ public String getColor() { } public List> getStations() { - return sections.getStations(); + return sections.getStationsResponse(); } public void update(Line updateLine) { diff --git a/src/main/java/nextstep/subway/line/LineController.java b/src/main/java/nextstep/subway/line/LineController.java index e1a6c81c88..536df3a7f2 100644 --- a/src/main/java/nextstep/subway/line/LineController.java +++ b/src/main/java/nextstep/subway/line/LineController.java @@ -1,5 +1,6 @@ package nextstep.subway.line; +import nextstep.subway.section.CreateSectionRequest; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -17,8 +18,8 @@ public LineController(LineService lineService) { } @PostMapping("/lines") - public ResponseEntity createLine(@RequestBody CreateLineDto dto) { - LineResponse response = lineService.register(dto); + public ResponseEntity createLine(@RequestBody CreateLineRequest request) { + LineResponse response = lineService.register(request); return ResponseEntity.created(URI.create("/lines/" + response.getId())).body(response); } @@ -33,8 +34,8 @@ public ResponseEntity showLine(@PathVariable("id") long id) { } @PutMapping("/lines/{id}") - public ResponseEntity updateLine(@PathVariable("id") long id, @RequestBody UpdateLineDto dto) { - lineService.update(id, dto); + public ResponseEntity updateLine(@PathVariable("id") long id, @RequestBody UpdateLineRequest request) { + lineService.update(id, request); return ResponseEntity.ok().build(); } @@ -43,4 +44,10 @@ public ResponseEntity deleteLine(@PathVariable("id") long id) { lineService.delete(id); return ResponseEntity.noContent().build(); } + + @PostMapping("/lines/{lineId}/sections") + public ResponseEntity createSection(@RequestBody CreateSectionRequest request, @PathVariable("lineId") long lineId) { + LineResponse response = lineService.registerSection(lineId, request); + return ResponseEntity.created(URI.create("/lines/" + response.getId())).body(response); + } } diff --git a/src/main/java/nextstep/subway/line/LineService.java b/src/main/java/nextstep/subway/line/LineService.java index c4872e69f2..207a8aad91 100644 --- a/src/main/java/nextstep/subway/line/LineService.java +++ b/src/main/java/nextstep/subway/line/LineService.java @@ -2,6 +2,7 @@ import nextstep.subway.section.Distance; import nextstep.subway.section.Section; +import nextstep.subway.section.CreateSectionRequest; import nextstep.subway.station.Station; import nextstep.subway.station.StationRepository; import org.springframework.stereotype.Service; @@ -23,20 +24,32 @@ public LineService(LineRepository lineRepository, StationRepository stationRepos } @Transactional - public LineResponse register(CreateLineDto dto) { - Station upStation = findStationById(dto.getUpStationId()); - Station downStation = findStationById(dto.getDownStationId()); - Distance distance = new Distance(dto.getDistance()); + public LineResponse registerSection(long id, CreateSectionRequest request) { + Line line = lineRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 노선입니다.")); + Station upStation = findStationById(request.getUpStationId()); + Station downStation = findStationById(request.getDownStationId()); + Distance distance = new Distance(request.getDistance()); + Section section = new Section(upStation, downStation, distance); + line.addSection(section); + return LineResponse.of(line); + } + + @Transactional + public LineResponse register(CreateLineRequest request) { + Line line = request.toLine(); + Station upStation = findStationById(request.getUpStationId()); + Station downStation = findStationById(request.getDownStationId()); + Distance distance = new Distance(request.getDistance()); Section section = new Section(upStation, downStation, distance); - Line line = dto.toLine(); line.addSection(section); return LineResponse.of(lineRepository.save(line)); } @Transactional - public void update(long id, UpdateLineDto dto) { + public void update(long id, UpdateLineRequest request) { Line line = findLineById(id); - line.update(dto.toLine()); + line.update(request.toLine()); } @Transactional(readOnly = true) diff --git a/src/main/java/nextstep/subway/line/UpdateLineDto.java b/src/main/java/nextstep/subway/line/UpdateLineRequest.java similarity index 91% rename from src/main/java/nextstep/subway/line/UpdateLineDto.java rename to src/main/java/nextstep/subway/line/UpdateLineRequest.java index ae76b93af5..45029ba159 100644 --- a/src/main/java/nextstep/subway/line/UpdateLineDto.java +++ b/src/main/java/nextstep/subway/line/UpdateLineRequest.java @@ -1,6 +1,6 @@ package nextstep.subway.line; -class UpdateLineDto { +class UpdateLineRequest { private String name; private String color; diff --git a/src/main/java/nextstep/subway/section/CreateSectionRequest.java b/src/main/java/nextstep/subway/section/CreateSectionRequest.java new file mode 100644 index 0000000000..7678df99a2 --- /dev/null +++ b/src/main/java/nextstep/subway/section/CreateSectionRequest.java @@ -0,0 +1,20 @@ +package nextstep.subway.section; + +public class CreateSectionRequest { + + private long upStationId; + private long downStationId; + private long distance; + + public long getUpStationId() { + return upStationId; + } + + public long getDownStationId() { + return downStationId; + } + + public long getDistance() { + return distance; + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/subway/section/Distance.java b/src/main/java/nextstep/subway/section/Distance.java index d53cf4057c..7b166e31eb 100644 --- a/src/main/java/nextstep/subway/section/Distance.java +++ b/src/main/java/nextstep/subway/section/Distance.java @@ -9,7 +9,37 @@ public class Distance { protected Distance() { } + public Distance(Distance distance) { + this.distance = distance.distance; + } + public Distance(Long distance) { this.distance = distance; } + + public Distance add(Distance distance) { + this.distance += distance.distance; + return this; + } + + public Distance subtract(Distance distance) { + this.distance -= distance.distance; + return this; + } + + public Boolean isZero() { + return distance == 0; + } + + public Boolean isNegative() { + return distance < 0; + } + + public Long get() { + return distance; + } + + public int compare(Distance distance) { + return Long.compare(this.distance, distance.distance); + } } diff --git a/src/main/java/nextstep/subway/section/Section.java b/src/main/java/nextstep/subway/section/Section.java index 67bd9eabc8..a954847300 100644 --- a/src/main/java/nextstep/subway/section/Section.java +++ b/src/main/java/nextstep/subway/section/Section.java @@ -27,6 +27,8 @@ public class Section { @Embedded private Distance distance; + private int sequence = 1; + protected Section() { } public Section(Station upStation, Station downStation, Distance distance) { @@ -46,4 +48,72 @@ public Station getUpStation() { public Station getDownStation() { return downStation; } + + public Distance getDistance() { + return distance; + } + + public void increaseSequence() { + this.sequence += 1; + } + + public void increaseSequence(int add) { + this.sequence = add + 1; + } + + public int getSequence() { + return sequence; + } + + public boolean isExtendDownStation(Section section) { + return this.downStation.equals(section.getUpStation()); + } + + public boolean isExtendUpStation(Section section) { + return this.upStation.equals(section.getDownStation()); + } + + public boolean isEqualUpStation(Section section) { + return this.upStation.equals(section.getUpStation()); + } + + public boolean isEqualDownStation(Section section) { + return this.downStation.equals(section.getDownStation()); + } + + public void replace(Section newSection, Distance totalSectionDistance) { + Distance excludeSectionDistance = totalSectionDistance.subtract(distance); + newSection.distance = newSection.distance.subtract(excludeSectionDistance); + newSection.upStation = upStation; + newSection.sequence = sequence; + upStation = newSection.downStation; + distance = distance.subtract(newSection.distance); + } + + public void syncUpStation(Section other, Distance distance) { + this.upStation = other.upStation; + this.distance = new Distance(other.distance).add(distance); + this.sequence = other.sequence; + other.upStation = this.downStation; + other.distance = other.distance.subtract(this.distance); + } + + public void syncDownStation(Section other, Distance distance) { + this.downStation = other.downStation; + this.distance = new Distance(other.distance).add(distance); + this.sequence = other.sequence + 1; + other.downStation = this.upStation; + other.distance = other.distance.subtract(this.distance); + } + + @Override + public String toString() { + return "Section{" + + "id=" + id + + ", sequence=" + sequence + + ", upStation=" + upStation.getName() + + ", downStation=" + downStation.getName() + + ", distance=" + distance.get() + + '}'; + } } diff --git a/src/main/java/nextstep/subway/section/Sections.java b/src/main/java/nextstep/subway/section/Sections.java index 93d4216f9d..7b085699ee 100644 --- a/src/main/java/nextstep/subway/section/Sections.java +++ b/src/main/java/nextstep/subway/section/Sections.java @@ -1,12 +1,11 @@ package nextstep.subway.section; +import nextstep.subway.station.Station; + import javax.persistence.CascadeType; import javax.persistence.Embeddable; import javax.persistence.OneToMany; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Embeddable @@ -17,19 +16,195 @@ public class Sections { public Sections() { } + public List> getStationsResponse() { + return getStations().stream() + .map(Station::toMapForOpen) + .collect(Collectors.toList()); + } + + private Set getStations() { + sort(); + Set stations = new LinkedHashSet<>(); + for (Section section : sections) { + stations.add(section.getUpStation()); + stations.add(section.getDownStation()); + } + return stations; + } + + private void sort() { + sections.sort(Comparator.comparingInt(Section::getSequence)); + } + public void add(Section section) { - sections.add(section); + if (addFirstSection(section)) { return; } + validateStationName(section); + sort(); + if (extendDownSection(section)) { return; } + if (extendUpSection(section)) { return; } + if (sliceDownSection(section)) { return; } + if (sliceUpSection(section)) { return; } + throw new IllegalArgumentException("구간 등록이 불가능한 알 수 없는 오류입니다."); } - public List> getStations() { + private boolean addFirstSection(Section section) { if (sections.size() == 0) { - return Collections.emptyList(); + sections.add(section); + return true; } - List> stations = sections.stream() - .map(section -> section.getUpStation().toMapForOpen()) - .collect(Collectors.toList()); - Section lastSection = sections.get(sections.size() - 1); - stations.add(lastSection.getDownStation().toMapForOpen()); - return stations; + return false; + } + + private boolean extendDownSection(Section section) { + int sectionLength = sections.size(); + if (sections.get(sectionLength - 1).isExtendDownStation(section)) { + section.increaseSequence(sectionLength); + sections.add(section); + return true; + } + return false; + } + + private boolean extendUpSection(Section section) { + if (sections.get(0).isExtendUpStation(section)) { + sections.forEach(Section::increaseSequence); + sections.add(section); + return true; + } + return false; + } + + private boolean sliceDownSection(Section section) { + Section downSection = findSectionEqualUpStation(section); + if (downSection == null) { + return false; + } + Distance distance = new Distance(section.getDistance()); + int startIndex = sections.indexOf(downSection); + sliceDownSection(section, distance, startIndex); + pushSequenceFromSlice(section); + sections.add(section); + return true; + } + + private void sliceDownSection(Section newSection, Distance distance, int startIndex) { + validateDownSectionDistance(distance, startIndex); + Section slice = null; + while (slice == null) { + Section targetSection = sections.get(startIndex); + distance.subtract(targetSection.getDistance()); + slice = sliceDownSection(newSection, targetSection, distance); + startIndex++; + } + } + + private Section sliceDownSection(Section newSection, Section targetSection, Distance distance) { + validateEqualDistance(distance); + if (distance.isNegative()) { + newSection.syncUpStation(targetSection, distance); + return newSection; + } + return null; + } + + private Section findSectionEqualUpStation(Section section) { + return sections.stream() + .filter(child -> child.isEqualUpStation(section)) + .findFirst() + .orElse(null); + } + + private boolean sliceUpSection(Section section) { + Section upSection = findSectionEqualDownStation(section); + if (upSection == null) { + return false; + } + Distance distance = new Distance(section.getDistance()); + sliceUpSection(section, distance, sections.indexOf(upSection)); + pushSequenceFromSlice(section); + sections.add(section); + return true; + } + + private void sliceUpSection(Section newSection, Distance distance, int endIndex) { + validateUpSectionDistance(distance, endIndex); + Section slice = null; + while (slice == null) { + Section targetSection = sections.get(endIndex); + distance.subtract(targetSection.getDistance()); + slice = sliceUpSection(newSection, targetSection, distance); + endIndex--; + } + } + + private Section sliceUpSection(Section newSection, Section targetSection, Distance distance) { + validateEqualDistance(distance); + if (distance.isNegative()) { + newSection.syncDownStation(targetSection, distance); + return newSection; + } + return null; + } + + private Section findSectionEqualDownStation(Section section) { + return sections.stream() + .filter(child -> child.isEqualDownStation(section)) + .findFirst() + .orElse(null); + } + + private void pushSequenceFromSlice(Section from) { + for (int index = from.getSequence() - 1; index < sections.size(); index++) { + sections.get(index) + .increaseSequence(); + } + } + + private void validateStationName(Section section) { + Set stations = getStations(); + Station upStation = section.getUpStation(); + Station downStation = section.getDownStation(); + if (stations.contains(upStation) && stations.contains(downStation)) { + throw new IllegalArgumentException("입력한 구간의 역은 모두 이미 등록되었습니다."); + } + if (!stations.contains(upStation) && !stations.contains(downStation)) { + throw new IllegalArgumentException("입력한 구간의 두 역 중 하나 이상은 등록되어 있어야 합니다."); + } + } + + private void validateDownSectionDistance(Distance distance, int startIndex) { + Distance totalDistance = new Distance(0L); + for (int index = startIndex; index < sections.size(); index++) { + totalDistance.add(sections.get(index).getDistance()); + } + validateExceedDistance(distance, totalDistance); + } + + private void validateUpSectionDistance(Distance distance, int endIndex) { + Distance totalDistance = new Distance(0L); + for (int index = 0; index <= endIndex; index++) { + totalDistance.add(sections.get(index).getDistance()); + } + validateExceedDistance(distance, totalDistance); + } + + private void validateExceedDistance(Distance distance, Distance totalDistance) { + if (distance.compare(totalDistance) == 1) { + throw new IllegalArgumentException("입력한 역의 길이가 종점까지의 거리를 초과했습니다."); + } + } + + private void validateEqualDistance(Distance distance) { + if (distance.isZero()) { + throw new IllegalArgumentException("해당 거리에 이미 역이 등록되었습니다."); + } + } + + @Override + public String toString() { + sort(); + return sections.stream() + .map(Section::toString) + .collect(Collectors.joining("\n")); } } diff --git a/src/main/java/nextstep/subway/station/Station.java b/src/main/java/nextstep/subway/station/Station.java index 62d85dc4a8..1f2178b332 100644 --- a/src/main/java/nextstep/subway/station/Station.java +++ b/src/main/java/nextstep/subway/station/Station.java @@ -5,6 +5,7 @@ import javax.persistence.*; import java.util.HashMap; import java.util.Map; +import java.util.Objects; @Entity public class Station extends BaseEntity { @@ -42,4 +43,17 @@ public Map toMapForOpen() { map.put("modifiedDate", getModifiedDate()); return map; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Station station = (Station) o; + return name.equals(station.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } } diff --git a/src/main/java/nextstep/subway/station/StationService.java b/src/main/java/nextstep/subway/station/StationService.java index b7f229e36d..ed8f2dfd81 100644 --- a/src/main/java/nextstep/subway/station/StationService.java +++ b/src/main/java/nextstep/subway/station/StationService.java @@ -16,8 +16,8 @@ public StationService(StationRepository stationRepository) { } @Transactional - public StationResponse saveStation(StationRequest stationRequest) { - Station persistStation = stationRepository.save(stationRequest.toStation()); + public StationResponse saveStation(StationRequest request) { + Station persistStation = stationRepository.save(request.toStation()); return StationResponse.of(persistStation); } diff --git a/src/test/java/nextstep/subway/line/LineServiceTest.java b/src/test/java/nextstep/subway/line/LineServiceTest.java index 3d6eb44d1a..3bce1ea0cd 100644 --- a/src/test/java/nextstep/subway/line/LineServiceTest.java +++ b/src/test/java/nextstep/subway/line/LineServiceTest.java @@ -41,13 +41,13 @@ void register() { String color = "Red"; Station 강남역 = new Station(1L, "강남역"); Station 판교역 = new Station(2L, "판교역"); - CreateLineDto dto = new CreateLineDto(name, color, 강남역.getId(), 판교역.getId(), 10); + CreateLineRequest request = new CreateLineRequest(name, color, 강남역.getId(), 판교역.getId(), 10); when(stationRepository.findById(1L)).thenReturn(Optional.of(강남역)); when(stationRepository.findById(2L)).thenReturn(Optional.of(판교역)); when(lineRepository.save(any(Line.class))).then(AdditionalAnswers.returnsFirstArg()); - LineResponse response = lineService.register(dto); + LineResponse response = lineService.register(request); assertAll( () -> assertThat(name).isEqualTo(response.getName()), () -> assertThat(color).isEqualTo(response.getColor()), diff --git a/src/test/java/nextstep/subway/line/LineTestFixture.java b/src/test/java/nextstep/subway/line/LineTestFixture.java index 2fd3440af9..f082fbb373 100644 --- a/src/test/java/nextstep/subway/line/LineTestFixture.java +++ b/src/test/java/nextstep/subway/line/LineTestFixture.java @@ -8,7 +8,7 @@ import java.util.HashMap; import java.util.Map; -class LineTestFixture { +public class LineTestFixture { public static ValidatableResponse create(String name, String color, StationResponse upStation, StationResponse downStation, int distance) { Map params = new HashMap<>(); @@ -25,6 +25,20 @@ public static ValidatableResponse create(String name, String color, StationRespo .then().log().all(); } + public static ValidatableResponse createSection(long lineId, StationResponse upStation, StationResponse downStation, int distance) { + Map params = new HashMap<>(); + params.put("upStationId", upStation.getId()); + params.put("downStationId", downStation.getId()); + params.put("distance", distance); + + return RestAssured.given().log().all() + .body(params) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .pathParam("id", lineId) + .when().post("/lines/{id}/sections") + .then().log().all(); + } + public static ValidatableResponse fetchAll() { return RestAssured.given().log().all() .when().get("/lines") diff --git a/src/test/java/nextstep/subway/section/SectionAcceptanceTest.java b/src/test/java/nextstep/subway/section/SectionAcceptanceTest.java new file mode 100644 index 0000000000..52181889cb --- /dev/null +++ b/src/test/java/nextstep/subway/section/SectionAcceptanceTest.java @@ -0,0 +1,115 @@ +package nextstep.subway.section; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.subway.common.AcceptanceTest; +import nextstep.subway.line.LineTestFixture; +import nextstep.subway.station.StationResponse; +import nextstep.subway.station.StationTestFixture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + + +@DisplayName("구간 인수 테스트") +class SectionAcceptanceTest extends AcceptanceTest { + + private StationResponse 강남역; + private StationResponse 상현역; + + private long lineId; + + // (공통) Given 이미 생성된 구간이 존재하고 + @BeforeEach + void init() { + 강남역 = StationTestFixture.create("강남역").extract().as(StationResponse.class); + 상현역 = StationTestFixture.create("상현역").extract().as(StationResponse.class); + lineId = LineTestFixture.create("신분당선", "bg-red-600", 강남역, 상현역, 100).extract().jsonPath().getLong("id"); + } + + /** + * When 구간 사이에 등록되지 않은 지하철 역을 중간에 등록하면 + * Then 지하철역이 등록된다. + */ + @DisplayName("역 사이에 새로운 역 등록") + @Test + void sliceSection() { + StationResponse 양재역 = StationTestFixture.create("양재역").extract().as(StationResponse.class); + StationResponse 판교역 = StationTestFixture.create("판교역").extract().as(StationResponse.class); + StationResponse 정자역 = StationTestFixture.create("정자역").extract().as(StationResponse.class); + + int 양재_응답_코드 = LineTestFixture.createSection(lineId, 강남역, 양재역, 30).extract().statusCode(); + int 판교_응답_코드 = LineTestFixture.createSection(lineId, 강남역, 판교역, 50).extract().statusCode(); + int 정자_응답_코드 = LineTestFixture.createSection(lineId, 강남역, 정자역, 60).extract().statusCode(); + + assertAll( + () -> assertThat(양재_응답_코드).isEqualTo(HttpStatus.CREATED.value()), + () -> assertThat(판교_응답_코드).isEqualTo(HttpStatus.CREATED.value()), + () -> assertThat(정자_응답_코드).isEqualTo(HttpStatus.CREATED.value()) + ); + } + + /* + * When 구간 사이에 등록되지 않은 지하철 역을 상행 종점으로 등록하면 + * Then 지하철 역이 등록된다. + * */ + @DisplayName("새로운 역을 상행 종점으로 등록") + @Test + void extendUpStationSection() { + StationResponse 신사역 = StationTestFixture.create("신사역").extract().as(StationResponse.class); + ExtractableResponse response = LineTestFixture.createSection(lineId, 신사역, 강남역, 30).extract(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + /* + * When 구간 사이에 등록되지 않은 지하철 역을 하행 종점으로 등록하면 + * Then 지하철 역이 등록된다. + * */ + @DisplayName("새로운 역을 하행 종점으로 등록") + @Test + void extendDownStationSection() { + StationResponse 광교중앙역 = StationTestFixture.create("광교중앙역").extract().as(StationResponse.class); + ExtractableResponse response = LineTestFixture.createSection(lineId, 상현역, 광교중앙역, 10).extract(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + /* + * When 구간 사이에 등록되지 않은 역과 길이를 기존 역 사이 길이보다 크게 지정해서 등록하면 + * Then 지하철 역이 등록되지 않는다. + * */ + @DisplayName("역 사이에 새로운 역을 등록하는 경우 기존 역 사이 길이보다 크거나 같으면 등록 불가") + @Test + void registerOverSectionLengthStationError() { + StationResponse 판교역 = StationTestFixture.create("판교역").extract().as(StationResponse.class); + ExtractableResponse response = LineTestFixture.createSection(lineId, 강남역, 판교역, 100).extract(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); + } + + /* + * When 구간 사이에 이미 등록된 상행, 하행 두 종점역을 등록하면 + * Then 지하철 역이 등록되지 않는다. + * */ + @DisplayName("이미 노선에 모두 등록된 상행 종점, 하행 종점역 등록") + @Test + void registerUpAndDownStationError() { + ExtractableResponse response = LineTestFixture.createSection(lineId, 강남역, 상현역, 50).extract(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); + } + + /* + * When 구간에 등록되지 않은 두 역을 등록하면 + * Then 지하철 역이 등록되지 않는다. + * */ + @DisplayName("등록하려는 두 역이 모두 노선에 포함되지 않은 경우") + @Test + void registerBothNoneStation() { + StationResponse 판교역 = StationTestFixture.create("판교역").extract().as(StationResponse.class); + StationResponse 정자역 = StationTestFixture.create("정자역").extract().as(StationResponse.class); + ExtractableResponse response = LineTestFixture.createSection(lineId, 판교역, 정자역, 50).extract(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); + } +} diff --git a/src/test/java/nextstep/subway/section/SectionsTest.java b/src/test/java/nextstep/subway/section/SectionsTest.java index 3f17020887..ef9e2239fe 100644 --- a/src/test/java/nextstep/subway/section/SectionsTest.java +++ b/src/test/java/nextstep/subway/section/SectionsTest.java @@ -1,35 +1,193 @@ package nextstep.subway.section; import nextstep.subway.station.Station; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.util.*; +import java.util.stream.Collectors; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; @DisplayName("구간 목록 도메인 테스트") class SectionsTest { - @DisplayName("구간이 등록된 경우 역 정보 목록 조회") + private Station 강남; + private Station 양재; + private Sections sections; + + @BeforeEach + void init() { + sections = new Sections(); + 강남 = new Station("강남"); + 양재 = new Station("양재"); + Section 강남_양재_구간 = new Section(강남, 양재, new Distance(50L)); + sections.add(강남_양재_구간); + } + + @AfterEach + void printResult() { + System.out.println(sections.toString()); + } + + @DisplayName("구간이 등록된 경우 정보 목록 조회") @Test void getStations() { + Station 광교중앙 = new Station("광교중앙영"); + Section 양재_광교중앙_구간 = new Section(양재, 광교중앙, new Distance(30L)); + sections.add(양재_광교중앙_구간); + assertThat(sections.getStationsResponse().size()).isEqualTo(3); + } + + @DisplayName("구간이 등록되지 않은 경우 정보 목록 조회") + @Test + void getEmptyStations() { Sections sections = new Sections(); - Station 신논현역 = new Station("신논현역"); - Station 강남역 = new Station("강남역"); - Station 양재역 = new Station("양재역"); + assertThat(sections.getStationsResponse().size()).isEqualTo(0); + } - Section 신분당_1구간 = new Section(신논현역, 강남역, new Distance(10L)); - Section 신분당_2구간 = new Section(강남역, 양재역, new Distance(10L)); + @DisplayName("새로운 상행 종점 등록") + @Test + void extendUpStation() { + Station 신사 = registerUpSection(강남, "신사", 10L); + assertStationNamesToUp(신사); + } - sections.add(신분당_1구간); - sections.add(신분당_2구간); + @DisplayName("새로운 하행 종점 등록") + @Test + void extendDownStation() { + Station 광교중앙 = registerDownSection(양재, "광교중앙", 30L); + assertStationNamesToDown(광교중앙); + } - assertThat(sections.getStations().size()).isEqualTo(3); + @DisplayName("하행 구간 사이 새로운 구간 등록 (시작으로부터 1구간 이내)") + @Test + void sliceDownSection1() { + Station 수지구청 = registerDownSection(양재, "수지구청", 50L); + Station 광교중앙 = registerDownSection(수지구청, "광교중앙", 30L); + Station 동천 = registerDownSection(양재, "동천", 40L); + assertStationNamesToDown(동천, 수지구청, 광교중앙); } - @DisplayName("구간이 등록되지 않은 경우 역 정보 목록 조회") + @DisplayName("하행 구간 사이 새로운 구간 등록 (시작으로부터 2구간 이내") @Test - void getEmptyStations() { - Sections sections = new Sections(); - assertThat(sections.getStations().size()).isEqualTo(0); + void sliceDownSection2() { + Station 동천 = registerDownSection(양재, "동천", 40L); + Station 수지구청 = registerDownSection(동천, "수지구청", 10L); + Station 광교중앙 = registerDownSection(수지구청, "광교중앙", 30L); + Station 상현 = registerDownSection(양재, "상현", 65L); + assertStationNamesToDown(동천, 수지구청, 상현, 광교중앙); + } + + @DisplayName("상행 구간 사이 새로운 구간 등록 (시작으로부터 1구간 이내)") + @Test + void sliceUpSection1() { + Station 동천 = registerDownSection(양재, "동천", 40L); + Station 수지 = registerDownSection(동천, "수지", 10L); + Station 판교 = registerUpSection(동천, "판교", 30L); + assertStationNamesToDown(판교, 동천, 수지); + } + + @DisplayName("상행 구간 사이 새로운 구간 등록 (시작으로부터 2구간 이내)") + @Test + void sliceUpSection2() { + Station 동천 = registerDownSection(양재, "동천", 40L); + Station 수지 = registerDownSection(동천, "수지", 10L); + Station 판교 = registerUpSection(수지, "판교", 40L); + assertStationNamesToDown(판교, 동천, 수지); + } + + @DisplayName("상행역과 하행역이 이미 노선에 모두 등록되어 오류 발생") + @Test + void duplicateStationError() { + Station 강남 = new Station("강남"); + Station 양재 = new Station("양재"); + Section 강남_양재_구간 = new Section(강남, 양재, new Distance(50L)); + + assertThatThrownBy(() -> { + sections.add(강남_양재_구간); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("입력한 구간의 역은 모두 이미 등록되었습니다."); + } + + @DisplayName("상행역, 하행역 둘 모두 포함되지 않은 경우 등록 오류 발생") + @Test + void notRegisteredStationError() { + Station 판교 = new Station("판교"); + Station 상현 = new Station("상현"); + Section 판교_상현_구간 = new Section(판교, 상현, new Distance(30L)); + + assertThatThrownBy(() -> { + sections.add(판교_상현_구간); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("입력한 구간의 두 역 중 하나 이상은 등록되어 있어야 합니다."); + } + + @DisplayName("역 사이 새로운 역을 등록하는 경우 기존 역 사이 길이와 같으면 오류 발생") + @Test + void alreadyLocatedStationError() { + Station 강남 = new Station("강남"); + Station 판교 = new Station("판교"); + Section 강남_판교_구간 = new Section(강남, 판교, new Distance(50L)); + + assertThatThrownBy(() -> { + sections.add(강남_판교_구간); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("해당 거리에 이미 역이 등록되었습니다."); + } + + @DisplayName("역 사이 새로운 역을 등록하는 경우 기존 역 사이보다 크면 오류 발생") + @Test + void exceedStationError() { + Station 강남 = new Station("강남"); + Station 판교 = new Station("판교"); + Section 강남_판교_구간 = new Section(강남, 판교, new Distance(60L)); + + assertThatThrownBy(() -> { + sections.add(강남_판교_구간); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("입력한 역의 길이가 종점까지의 거리를 초과했습니다."); + } + + private List getStationNames() { + List> stations = sections.getStationsResponse(); + return stations.stream() + .map(station -> station.get("name").toString()) + .collect(Collectors.toList()); + } + + private Station registerDownSection(Station upStation, String downStationName, Long distance) { + Station downStation = new Station(downStationName); + Section section = new Section(upStation, downStation, new Distance(distance)); + sections.add(section); + return downStation; + } + + private Station registerUpSection(Station downStation, String upStationName, Long distance) { + Station upStation = new Station(upStationName); + Section section = new Section(upStation, downStation, new Distance(distance)); + sections.add(section); + return upStation; + } + + private void assertStationNamesToDown(Station... stations) { + List names = new ArrayList<>(Arrays.asList(강남.getName(), 양재.getName())); + Arrays.stream(stations).forEach(station -> names.add(station.getName())); + String[] collect = names.toArray(new String[0]); + assertThat(getStationNames()).containsExactly(collect); + } + + private void assertStationNamesToUp(Station... stations) { + List names = Arrays.stream(stations) + .map(Station::getName) + .collect(Collectors.toList()); + Collections.reverse(names); + names.add(강남.getName()); + names.add(양재.getName()); + String[] collect = names.toArray(new String[0]); + assertThat(getStationNames()).containsExactly(collect); } } \ No newline at end of file