Skip to content

Commit

Permalink
[터틀] API 테스트/문서자동화 미션 제출합니다. (#11)
Browse files Browse the repository at this point in the history
* feat : 문서 자동화 실습 진행

- Spring REST Docs 의존성 추가
- MemberControllerTest 내용 복사/붙여넣기
- AuthAcceptanceTest 임시 비활성화
- LineControllerTest를 SpringBootTest로 변경
- MemberDocumentation 주석 해제

* feat : 사용자 인증 실습 진행

- Utf8EncodingFilter 작성
- Session / Basic / Bearer AuthInterceptor 작성
- MockMvc 사용하여 AuthAcceptanceTest 작성

* docs : 요구사항 작성

* feat : 회원 정보 관리 인수테스트 작성

- JWT 토큰 활용하여 회원정보 CRUD시 인증절차 거치도록 구현
- 인증이 필요한 컨트롤러들의 패턴을 BearerAuthInterceptor에 추가
- 인증정보(LoginMember)가 없을 경우 200 상태로 빈 멤버를 응답
- 인증정보가 틀릴 경우 401 응답
- 사용하지 않는 다른 인증 및 관련 테스트 삭제
- 서버 프로젝트 스코프 컨벤션 리팩토링 진행

* feat : 회원 CRUD 단위 테스트 작성

* refactor : RestAssured 테스트 픽스쳐들을 MockMvc 픽스처들로 대체

- RestAssured 의존성 삭제

* feat : 회원관리 컨트롤러 테스트 작성 및 문서 자동화

* refactor : MockMvc 테스트 픽스쳐 리팩토링

* etc : 프론트엔드 샘플 코드 추가 및 1단계 요구사항 반영

* feat : Favorite CRD 인수, 단위 테스트 및 구현

* feat : Favorite 컨트롤러 테스트 작성 및 문서화

* feat : Favorite 페이지 연동

- Login.js에서 비밀번호 input keypress event에 onLogin 추가

* refactor : 컨벤션에 맞게 프로젝트 스코프 리팩토링

* refactor : 컨트롤러에서의 사용자 검증을 LoginMemberMethodArgumentResolver, LoginMember.Type으로 이동

* refactor : GlobalExceptionHandler 및 커스텀 예외처리 추가

* test : 일부 AcceptanceTest 다이나믹 테스트 적용 및 Fixture 리팩토링

* refactor: MemberDto에 기본 생성자 추가

* test : 토큰 생성 실패 테스트 추가

* test : LoginMemberControllerTest 추가

* refactor : 전반적인 리팩토링

- 클래스 수준의 RequestMapping 추가
- 메소드 이름 변경
- dependency-management를 통해 버전관리하도록 변경

* refactor: MemberService에서 FavoriteService를 분리

* refactor: Favorite을 생성할 때 데이터베이스 접근을 1회로 변경

* refactor: 하드코딩 값을 상수로 변경

* refactor: default Exception Handler 추가

Co-authored-by: Woonjang Ahn <woonjangahn@gmail.com>
  • Loading branch information
begaonnuri and woonjangahn authored Jun 11, 2020
1 parent 63a6117 commit c23ec1b
Show file tree
Hide file tree
Showing 91 changed files with 3,208 additions and 1,361 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# atdd-subway-favorite

## 1단계 - 회원관리 기능

### 회원정보 수정 기능

### 요구 사항

- [x] 회원 정보를 관리하는 기능 구현
- [x] 자신의 정보만 수정 가능하도록 해야하며 로그인이 선행되어야 함
- [x] 토큰의 유효성 검사와 본인 여부를 판단하는 로직 추가
- [x] side case에 대한 예외처리
- [x] 인수 테스트와 단위 테스트 작성
- [x] API 문서를 작성하고 문서화를 위한 테스트 작성
- [x] 페이지 연동

### 시나리오

```gherkin
Scenario: 로그인
Given 회원가입이 되어있다.
When 사용자는 이메일과 비밀번호로 로그인을 요청한다.
Then 로그인이 된다.
And 기존 페이지로 돌아간다.
```

## 2단계 - 즐겨찾기 기능

### 즐겨찾기 기능

### 요구사항

- [x] 즐겨찾기 기능을 추가(추가,삭제,조회)
- [x] 자신의 정보만 수정 가능하도록 해야하며 로그인이 선행되어야 함
- [x] 토큰의 유효성 검사와 본인 여부를 판단하는 로직 추가(interceptor, argument resolver)
- [ ] side case에 대한 예외처리 필수
- [x] 인수 테스트와 단위 테스트 작성
- [ ] API 문서를 작성하고 문서화를 위한 테스트 작성
- [ ] 페이지 연동
51 changes: 35 additions & 16 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,31 +1,50 @@
plugins {
id 'org.springframework.boot' version '2.2.5.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
id 'org.springframework.boot' version '2.2.5.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id "org.asciidoctor.convert" version "1.5.9.2"
id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
mavenCentral()
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1'
implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0'
implementation 'org.jgrapht:jgrapht-core:1.0.1'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
testImplementation 'io.rest-assured:rest-assured:3.3.0'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1'
implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0'
implementation 'org.jgrapht:jgrapht-core:1.0.1'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
runtimeOnly 'com.h2database:h2'
}

ext {
snippetsDir = file('build/generated-snippets')
}

test {
useJUnitPlatform()
useJUnitPlatform()
outputs.dir snippetsDir
}

asciidoctor {
inputs.dir snippetsDir
dependsOn test
}

bootJar {
dependsOn asciidoctor
from("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
50 changes: 44 additions & 6 deletions src/docs/asciidoc/api-guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,56 @@ endif::[]
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:toclevels: 3
:sectlinks:
:operation-http-request-title: Example Request
:operation-http-response-title: Example Response
:operation-path-parameters-title: 경로 파라미터
:operation-request-headers-title: 요청 헤더
:operation-request-body-title: 요청 바디
:operation-request-fields-title: 요청 필드
:operation-request-parameters-title: 요청 파라미터
:operation-http-request-title: 요청 예시
:operation-http-response-title: 응답 예시

[[resources]]
= Resources
= 우아한지하철 서비스 어드민

[[resources-members]]
== Member
== 회원 관리

[[resources-members-create]]
=== 회원 가입

operation::members/create[snippets='http-request,http-response']
operation::members/create[snippets='request-fields,request-body,http-request,http-response']

[[resources-members-findByEmail]]
=== 회원 정보 조회

operation::members/findByEmail[snippets='request-headers,request-parameters,http-request,http-response']

[[resources-members-update]]
=== 회원 정보 수정

operation::members/update[snippets='request-headers,path-parameters,request-fields,request-body,http-request,http-response']

[[resources-member-delete]]
=== 회원 정보 삭제

operation::members/delete[snippets='request-headers,path-parameters,http-request,http-response']

[[resources-favorites]]
== 즐겨찾기 관리

[[resources-favorites-add]]
=== 즐겨찾기 추가

operation::favorites/add[snippets='request-headers,request-fields,request-body,http-request,http-response']

[[resources-favorites-getAll]]
=== 즐겨찾기 목록 조회

operation::favorites/getAll[snippets='request-headers,http-request,http-response']

[[resources-favorites-remove]]
=== 즐겨찾기 제거

operation::favorites/remove[snippets='request-headers,path-parameters,http-request,http-response']
3 changes: 2 additions & 1 deletion src/main/java/wooteco/subway/config/ETagHeaderFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
public class ETagHeaderFilter {
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean
= new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
filterRegistrationBean.addUrlPatterns("/lines/detail");
filterRegistrationBean.setName("etagFilter");
return filterRegistrationBean;
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/wooteco/subway/config/Utf8EncodingFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package wooteco.subway.config;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CharacterEncodingFilter;

@Configuration
public class Utf8EncodingFilter {

private static final String ENCODING = "UTF-8";

@Bean
public FilterRegistrationBean<CharacterEncodingFilter> utf8CharacterEncodingFilter() {
return new FilterRegistrationBean<>(new CharacterEncodingFilter(ENCODING, true));
}
}
31 changes: 15 additions & 16 deletions src/main/java/wooteco/subway/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -1,41 +1,40 @@
package wooteco.subway.config;

import java.util.List;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import wooteco.subway.web.member.LoginMemberMethodArgumentResolver;
import wooteco.subway.web.member.interceptor.BasicAuthInterceptor;
import wooteco.subway.web.member.interceptor.BearerAuthInterceptor;
import wooteco.subway.web.member.interceptor.SessionInterceptor;

import java.util.List;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final BasicAuthInterceptor basicAuthInterceptor;
private final SessionInterceptor sessionInterceptor;
private final BearerAuthInterceptor bearerAuthInterceptor;
private final LoginMemberMethodArgumentResolver loginMemberArgumentResolver;

public WebMvcConfig(BasicAuthInterceptor basicAuthInterceptor,
SessionInterceptor sessionInterceptor,
BearerAuthInterceptor bearerAuthInterceptor,
LoginMemberMethodArgumentResolver loginMemberArgumentResolver) {
this.basicAuthInterceptor = basicAuthInterceptor;
this.sessionInterceptor = sessionInterceptor;
public WebMvcConfig(
BearerAuthInterceptor bearerAuthInterceptor,
LoginMemberMethodArgumentResolver loginMemberArgumentResolver
) {
this.bearerAuthInterceptor = bearerAuthInterceptor;
this.loginMemberArgumentResolver = loginMemberArgumentResolver;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(basicAuthInterceptor).addPathPatterns("/me/basic");
registry.addInterceptor(sessionInterceptor).addPathPatterns("/me/session");
registry.addInterceptor(bearerAuthInterceptor).addPathPatterns("/me/bearer");
registry.addInterceptor(bearerAuthInterceptor)
.addPathPatterns("/me/bearer")
.addPathPatterns("/members")
.addPathPatterns("/members/*")
.addPathPatterns("/favorites")
.addPathPatterns("/favorites/*");
}

@Override
public void addArgumentResolvers(List argumentResolvers) {
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(loginMemberArgumentResolver);
}
}
41 changes: 19 additions & 22 deletions src/main/java/wooteco/subway/domain/line/LineStations.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package wooteco.subway.domain.line;

import java.util.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

public class LineStations {
private Set<LineStation> stations;
Expand Down Expand Up @@ -29,7 +34,7 @@ private void remove(LineStation targetLineStation) {

public void removeById(Long targetStationId) {
extractByStationId(targetStationId)
.ifPresent(this::remove);
.ifPresent(this::remove);
}

public List<Long> getStationIds() {
Expand All @@ -40,37 +45,29 @@ public List<Long> getStationIds() {

private void extractNext(Long preStationId, List<Long> ids) {
stations.stream()
.filter(it -> Objects.equals(it.getPreStationId(), preStationId))
.findFirst()
.ifPresent(it -> {
Long nextStationId = it.getStationId();
ids.add(nextStationId);
extractNext(nextStationId, ids);
});
.filter(it -> Objects.equals(it.getPreStationId(), preStationId))
.findFirst()
.ifPresent(it -> {
Long nextStationId = it.getStationId();
ids.add(nextStationId);
extractNext(nextStationId, ids);
});
}

private void updatePreStationOfNextLineStation(Long targetStationId, Long newPreStationId) {
extractByPreStationId(targetStationId)
.ifPresent(it -> it.updatePreLineStation(newPreStationId));
.ifPresent(it -> it.updatePreLineStation(newPreStationId));
}

private Optional<LineStation> extractByStationId(Long stationId) {
return stations.stream()
.filter(it -> Objects.equals(it.getStationId(), stationId))
.findFirst();
.filter(it -> Objects.equals(it.getStationId(), stationId))
.findFirst();
}

private Optional<LineStation> extractByPreStationId(Long preStationId) {
return stations.stream()
.filter(it -> Objects.equals(it.getPreStationId(), preStationId))
.findFirst();
}

public int getTotalDistance() {
return stations.stream().mapToInt(it -> it.getDistance()).sum();
}

public int getTotalDuration() {
return stations.stream().mapToInt(it -> it.getDuration()).sum();
.filter(it -> Objects.equals(it.getPreStationId(), preStationId))
.findFirst();
}
}
26 changes: 3 additions & 23 deletions src/main/java/wooteco/subway/domain/line/Lines.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,8 @@ public List<Line> getLines() {

public List<Long> getStationIds() {
return lines.stream()
.flatMap(it -> it.getStations().stream())
.map(it -> it.getStationId())
.collect(Collectors.toList());
.flatMap(it -> it.getStations().stream())
.map(it -> it.getStationId())
.collect(Collectors.toList());
}
//
// public LineStations extractLineStationByStationIds(List<Long> stationIds) {
// for (Long stationId : stationIds) {
// List<LineStation> lineStations = findLineStation(null, stationId);
// }
// Set<LineStation> lineStations = stationIds.stream()
// .map(it -> getLineStationByStationId(it))
// .collect(Collectors.toSet());
//
// return new LineStations(lineStations);
// }
//
// private LineStation getLineStationByStationId(Long stationId) {
// return lines.stream()
// .flatMap(it -> it.getStations().stream())
//// .filter(it -> Objects.nonNull(it.getPreStationId()))
// .filter(it -> it.getStationId() == stationId)
// .findFirst()
// .orElseThrow(RuntimeException::new);
// }
}
Loading

0 comments on commit c23ec1b

Please sign in to comment.