From 82e4db7e2a9872d9c183f9bc5df4606f1ddf4f19 Mon Sep 17 00:00:00 2001 From: JFe <33208246+Go-Jaecheol@users.noreply.github.com> Date: Thu, 14 Sep 2023 23:08:37 +0900 Subject: [PATCH] =?UTF-8?q?[=ED=86=B0=EC=BA=A3=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20-=203,=204=EB=8B=A8=EA=B3=84]=20=EB=A7=9D?= =?UTF-8?q?=EA=B3=A0(=EA=B3=A0=EC=9E=AC=EC=B2=A0)=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=A0=9C=EC=B6=9C=ED=95=A9=EB=8B=88=EB=8B=A4.=20(#460)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: 3, 4단계 요구사항 정리 * refactor: RequestLine 객체 생성 * refactor: RequestHeader 객체 생성 * refactor: HttpRequest 객체 분리 * refactor: StatusLine 객체 생성 * refactor: ResponseHeader 객체 생성 * refactor: HttpResponse 객체 분리 * feat: ResponseBody 객체 추가 * test: HttpResponse toString 테스트 추가 * feat: HttpResponse 빈 객체 생성자 추가 * feat: Controller 인터페이스 추가 * feat: HomeController 구현 * feat: ResourceController 구현 * feat: RegisterController 구현 * feat: LoginController 구현 * refactor: RequestMapping 객체 추가 * test: 캐시 학습 테스트 완료 * test: Thread 학습 테스트 완료 * feat: Thread Pool 적용 * refactor: SessionManager 동시성 컬렉션 적용 * refactor: 405 Method Not Allowed 반영 * refactor: HttpMethod enum 생성 * refactor: Request Parameter 구하는 책임 변경 * refactor: HttpRequest에서 Cookie 값 반환하도록 책임 변경 * refactor: httpRequest, httpResponse 변수명 통일 --- README.md | 9 + .../example/cachecontrol/CacheWebConfig.java | 6 + .../example/etag/EtagFilterConfiguration.java | 15 +- .../version/CacheBustingWebConfig.java | 6 +- study/src/main/resources/application.yml | 3 + .../thread/stage0/SynchronizationTest.java | 2 +- .../java/thread/stage0/ThreadPoolsTest.java | 6 +- .../jwp/controller/AbstractController.java | 37 ++++ .../nextstep/jwp/controller/Controller.java | 11 + .../jwp/controller/HomeController.java | 26 +++ .../jwp/controller/LoginController.java | 78 +++++++ .../jwp/controller/RegisterController.java | 37 ++++ .../jwp/controller/ResourceController.java | 36 ++++ .../jwp/exception/HttpRequestException.java | 8 + .../apache/catalina/connector/Connector.java | 11 +- .../apache/coyote/http11/Http11Processor.java | 202 ++---------------- .../apache/coyote/http11/RequestMapping.java | 27 +++ .../apache/coyote/http11/SessionManager.java | 4 +- .../coyote/http11/request/HttpMethod.java | 5 + .../coyote/http11/request/HttpRequest.java | 94 ++++++++ .../coyote/http11/request/RequestHeader.java | 33 +++ .../coyote/http11/request/RequestLine.java | 31 +++ .../coyote/http11/response/HttpResponse.java | 57 +++++ .../coyote/http11/response/HttpStatus.java | 25 +++ .../coyote/http11/response/ResponseBody.java | 38 ++++ .../http11/response/ResponseHeader.java | 35 +++ .../coyote/http11/response/StatusLine.java | 29 +++ .../jwp/controller/HomeControllerTest.java | 96 +++++++++ .../jwp/controller/LoginControllerTest.java | 101 +++++++++ .../controller/RegisterControllerTest.java | 102 +++++++++ .../controller/ResourceControllerTest.java | 97 +++++++++ .../coyote/http11/Http11ProcessorTest.java | 23 +- .../coyote/http11/RequestMappingTest.java | 127 +++++++++++ .../http11/request/HttpRequestTest.java | 59 +++++ .../http11/request/RequestHeaderTest.java | 118 ++++++++++ .../http11/request/RequestLineTest.java | 34 +++ .../http11/response/HttpResponseTest.java | 78 +++++++ .../http11/response/ResponseBodyTest.java | 50 +++++ .../http11/response/ResponseHeaderTest.java | 60 ++++++ .../http11/response/StatusLineTest.java | 47 ++++ 40 files changed, 1649 insertions(+), 214 deletions(-) create mode 100644 tomcat/src/main/java/nextstep/jwp/controller/AbstractController.java create mode 100644 tomcat/src/main/java/nextstep/jwp/controller/Controller.java create mode 100644 tomcat/src/main/java/nextstep/jwp/controller/HomeController.java create mode 100644 tomcat/src/main/java/nextstep/jwp/controller/LoginController.java create mode 100644 tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java create mode 100644 tomcat/src/main/java/nextstep/jwp/controller/ResourceController.java create mode 100644 tomcat/src/main/java/nextstep/jwp/exception/HttpRequestException.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/response/ResponseBody.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/response/ResponseHeader.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/response/StatusLine.java create mode 100644 tomcat/src/test/java/nextstep/jwp/controller/HomeControllerTest.java create mode 100644 tomcat/src/test/java/nextstep/jwp/controller/LoginControllerTest.java create mode 100644 tomcat/src/test/java/nextstep/jwp/controller/RegisterControllerTest.java create mode 100644 tomcat/src/test/java/nextstep/jwp/controller/ResourceControllerTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/RequestMappingTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/request/RequestHeaderTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/request/RequestLineTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/response/ResponseBodyTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/response/ResponseHeaderTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/response/StatusLineTest.java diff --git a/README.md b/README.md index af83630d50..cea9ee57e2 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,12 @@ - [x] POST 방식으로 회원가입 - [x] Cookie에 JSESSIONID 값 저장하기 - [x] Session 구현하기 + +- 3단계 - 리팩터링 + - [x] HttpRequest 클래스 구현하기 + - [x] HttpResponse 클래스 구현하기 + - [x] Controller 인터페이스 추가하기 + +- 4단계 - 동시성 확장하기 + - [x] Executors로 Thread Pool 적용 + - [x] 동시성 컬렉션 사용하기 diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java index 305b1f1e1e..6a45567463 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,13 +1,19 @@ package cache.com.example.cachecontrol; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.WebContentInterceptor; @Configuration public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + final var cacheControl = CacheControl.noCache().cachePrivate(); + final var webContentInterceptor = new WebContentInterceptor(); + webContentInterceptor.addCacheMapping(cacheControl, "/**"); + registry.addInterceptor(webContentInterceptor); } } diff --git a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java index 41ef7a3d9a..b4b89d5e22 100644 --- a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java +++ b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java @@ -1,12 +1,19 @@ package cache.com.example.etag; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.ShallowEtagHeaderFilter; + +import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES; @Configuration public class EtagFilterConfiguration { -// @Bean -// public FilterRegistrationBean shallowEtagHeaderFilter() { -// return null; -// } + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + final var filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + filterRegistrationBean.addUrlPatterns("/etag", PREFIX_STATIC_RESOURCES + "/*"); + return filterRegistrationBean; + } } diff --git a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java index 6da6d2c795..5a422e5b2e 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -2,9 +2,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.time.Duration; + @Configuration public class CacheBustingWebConfig implements WebMvcConfigurer { @@ -20,6 +23,7 @@ public CacheBustingWebConfig(ResourceVersion version) { @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") - .addResourceLocations("classpath:/static/"); + .addResourceLocations("classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic()); } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..385c11d5f1 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -7,3 +7,6 @@ server: max-connections: 1 threads: max: 2 + compression: + enabled: true + min-response-size: 10 diff --git a/study/src/test/java/thread/stage0/SynchronizationTest.java b/study/src/test/java/thread/stage0/SynchronizationTest.java index 0333c18e3b..b463c2b984 100644 --- a/study/src/test/java/thread/stage0/SynchronizationTest.java +++ b/study/src/test/java/thread/stage0/SynchronizationTest.java @@ -41,7 +41,7 @@ private static final class SynchronizedMethods { private int sum = 0; - public void calculate() { + public synchronized void calculate() { setSum(getSum() + 1); } diff --git a/study/src/test/java/thread/stage0/ThreadPoolsTest.java b/study/src/test/java/thread/stage0/ThreadPoolsTest.java index 238611ebfe..03efdabc8d 100644 --- a/study/src/test/java/thread/stage0/ThreadPoolsTest.java +++ b/study/src/test/java/thread/stage0/ThreadPoolsTest.java @@ -31,8 +31,8 @@ void testNewFixedThreadPool() { executor.submit(logWithSleep("hello fixed thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; - final int expectedQueueSize = 0; + final int expectedPoolSize = 2; + final int expectedQueueSize = 1; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size()); @@ -46,7 +46,7 @@ void testNewCachedThreadPool() { executor.submit(logWithSleep("hello cached thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; + final int expectedPoolSize = 3; final int expectedQueueSize = 0; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); diff --git a/tomcat/src/main/java/nextstep/jwp/controller/AbstractController.java b/tomcat/src/main/java/nextstep/jwp/controller/AbstractController.java new file mode 100644 index 0000000000..d5bbb2a27e --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/AbstractController.java @@ -0,0 +1,37 @@ +package nextstep.jwp.controller; + +import nextstep.jwp.exception.HttpRequestException; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; + +public abstract class AbstractController implements Controller { + + protected static final String TEXT_HTML = "text/html;charset=utf-8"; + protected static final String TEXT_CSS = "text/css;"; + protected static final String INDEX_PAGE = "/index.html"; + protected static final String UNAUTHORIZED_PAGE = "/401.html"; + protected static final String HEADER_LOCATION = "Location"; + protected static final String HEADER_SET_COOKIE = "Set-Cookie"; + protected static final String HEADER_CONTENT_TYPE = "Content-Type"; + protected static final String HEADER_CONTENT_LENGTH = "Content-Length"; + protected static final String HTTP_METHOD_EXCEPTION_MESSAGE = "올바르지 않은 HTTP Method 입니다."; + + @Override + public void service(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException { + if (httpRequest.getRequestLine().getMethod().equals("POST")) { + doPost(httpRequest, httpResponse); + return; + } + if (httpRequest.getRequestLine().getMethod().equals("GET")) { + doGet(httpRequest, httpResponse); + return; + } + throw new HttpRequestException(HTTP_METHOD_EXCEPTION_MESSAGE); + } + + protected abstract void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException; + + protected abstract void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException; +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/Controller.java b/tomcat/src/main/java/nextstep/jwp/controller/Controller.java new file mode 100644 index 0000000000..c5ffa07fb4 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/Controller.java @@ -0,0 +1,11 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; + +public interface Controller { + + void service(final HttpRequest request, final HttpResponse httpResponse) throws IOException; +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java b/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java new file mode 100644 index 0000000000..c193a90348 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java @@ -0,0 +1,26 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpStatus; +import org.apache.coyote.http11.response.ResponseBody; +import org.apache.coyote.http11.response.StatusLine; + +public class HomeController extends AbstractController { + + @Override + protected void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) { + final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.METHOD_NOT_ALLOWED); + httpResponse.setStatusLine(statusLine); + } + + @Override + protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) { + final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.OK); + final var responseBody = ResponseBody.fromText("Hello world!"); + httpResponse.setStatusLine(statusLine); + httpResponse.addResponseHeader("Content-Type", TEXT_HTML); + httpResponse.addResponseHeader("Content-Length", String.valueOf(responseBody.getBody().getBytes().length)); + httpResponse.setResponseBody(responseBody); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java new file mode 100644 index 0000000000..8550fb3f19 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java @@ -0,0 +1,78 @@ +package nextstep.jwp.controller; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.http11.HttpCookie; +import org.apache.coyote.http11.Session; +import org.apache.coyote.http11.SessionManager; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpStatus; +import org.apache.coyote.http11.response.ResponseBody; +import org.apache.coyote.http11.response.StatusLine; + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; + +public class LoginController extends AbstractController { + + private static final SessionManager SESSION_MANAGER = new SessionManager(); + + @Override + protected void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) { + final HttpCookie cookie = httpRequest.getCookie(); + final User user = findUserBySessionId(cookie.getJSessionId()); + final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.FOUND); + httpResponse.setStatusLine(statusLine); + if (user == null) { + handleFirstLogin(httpRequest, httpResponse); + return; + } + httpResponse.addResponseHeader(HEADER_LOCATION, INDEX_PAGE); + } + + private void handleFirstLogin(final HttpRequest httpRequest, final HttpResponse httpResponse) { + final Map requestBodyValues = httpRequest.getRequestParameters(); + final Optional user = InMemoryUserRepository.findByAccount(requestBodyValues.get("account")); + if (user.isEmpty() || !user.get().checkPassword(requestBodyValues.get("password"))) { + httpResponse.addResponseHeader(HEADER_LOCATION, UNAUTHORIZED_PAGE); + return; + } + final String sessionId = addSession(user.get()); + httpResponse.addResponseHeader(HEADER_LOCATION, INDEX_PAGE); + httpResponse.addResponseHeader(HEADER_SET_COOKIE, "JSESSIONID=" + sessionId); + } + + @Override + protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException { + final HttpCookie cookie = httpRequest.getCookie(); + final User user = findUserBySessionId(cookie.getJSessionId()); + if (user != null) { + doPost(httpRequest, httpResponse); + return; + } + final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.OK); + final var responseBody = ResponseBody.fromUri("/login.html"); + httpResponse.setStatusLine(statusLine); + httpResponse.addResponseHeader(HEADER_CONTENT_TYPE, TEXT_HTML); + httpResponse.addResponseHeader(HEADER_CONTENT_LENGTH, String.valueOf(responseBody.getBody().getBytes().length)); + httpResponse.setResponseBody(responseBody); + } + + private User findUserBySessionId(final String sessionId) { + if (sessionId == null) { + return null; + } + final Session session = SESSION_MANAGER.findSession(sessionId) + .orElseGet(Session::create); + return (User) session.getAttribute("user"); + } + + private String addSession(final User user) { + final var session = Session.create(); + session.setAttribute("user", user); + SESSION_MANAGER.add(session); + return session.getId(); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java new file mode 100644 index 0000000000..bdc32dc7c7 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java @@ -0,0 +1,37 @@ +package nextstep.jwp.controller; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpStatus; +import org.apache.coyote.http11.response.ResponseBody; +import org.apache.coyote.http11.response.StatusLine; + +import java.io.IOException; +import java.util.Map; + +public class RegisterController extends AbstractController { + + @Override + protected void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) { + final Map requestBodyValues = httpRequest.getRequestParameters(); + final var user = new User(requestBodyValues.get("account"), requestBodyValues.get("password"), + requestBodyValues.get("email")); + InMemoryUserRepository.save(user); + + final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.FOUND); + httpResponse.setStatusLine(statusLine); + httpResponse.addResponseHeader("Location", INDEX_PAGE); + } + + @Override + protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException { + final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.OK); + final var responseBody = ResponseBody.fromUri("/register.html"); + httpResponse.setStatusLine(statusLine); + httpResponse.addResponseHeader("Content-Type", TEXT_HTML); + httpResponse.addResponseHeader("Content-Length", String.valueOf(responseBody.getBody().getBytes().length)); + httpResponse.setResponseBody(responseBody); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/ResourceController.java b/tomcat/src/main/java/nextstep/jwp/controller/ResourceController.java new file mode 100644 index 0000000000..c58a0d5ef6 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/ResourceController.java @@ -0,0 +1,36 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpStatus; +import org.apache.coyote.http11.response.ResponseBody; +import org.apache.coyote.http11.response.StatusLine; + +import java.io.IOException; + +public class ResourceController extends AbstractController { + + @Override + protected void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) { + final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.METHOD_NOT_ALLOWED); + httpResponse.setStatusLine(statusLine); + } + + @Override + protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException { + final var statusLine = StatusLine.of(httpRequest.getRequestLine().getProtocol(), HttpStatus.OK); + final String uri = httpRequest.getRequestLine().getPath().split("\\?")[0]; + final var responseBody = ResponseBody.fromUri(uri); + httpResponse.setStatusLine(statusLine); + httpResponse.addResponseHeader("Content-Type", getContentType(uri)); + httpResponse.addResponseHeader("Content-Length", String.valueOf(responseBody.getBody().getBytes().length)); + httpResponse.setResponseBody(responseBody); + } + + private String getContentType(final String uri) { + if (uri.endsWith(".css")) { + return TEXT_CSS; + } + return TEXT_HTML; + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/exception/HttpRequestException.java b/tomcat/src/main/java/nextstep/jwp/exception/HttpRequestException.java new file mode 100644 index 0000000000..236f600558 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/exception/HttpRequestException.java @@ -0,0 +1,8 @@ +package nextstep.jwp.exception; + +public class HttpRequestException extends RuntimeException { + + public HttpRequestException(final String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java index 3b2c4dda7c..21a4cfb895 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -8,6 +8,8 @@ import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class Connector implements Runnable { @@ -15,17 +17,20 @@ public class Connector implements Runnable { private static final int DEFAULT_PORT = 8080; private static final int DEFAULT_ACCEPT_COUNT = 100; + private static final int MAX_THREAD_POOL_SIZE = 250; + private final ExecutorService executorService; private final ServerSocket serverSocket; private boolean stopped; public Connector() { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); + this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, MAX_THREAD_POOL_SIZE); } - public Connector(final int port, final int acceptCount) { + public Connector(final int port, final int acceptCount, final int maxThreads) { this.serverSocket = createServerSocket(port, acceptCount); this.stopped = false; + this.executorService = Executors.newFixedThreadPool(maxThreads); } private ServerSocket createServerSocket(final int port, final int acceptCount) { @@ -67,7 +72,7 @@ private void process(final Socket connection) { return; } var processor = new Http11Processor(connection); - new Thread(processor).start(); + executorService.execute(processor); } public void stop() { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 616f4b60f1..b773850cda 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,35 +1,21 @@ package org.apache.coyote.http11; -import nextstep.jwp.db.InMemoryUserRepository; import nextstep.jwp.exception.UncheckedServletException; -import nextstep.jwp.model.User; import org.apache.coyote.Processor; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.Socket; -import java.net.URL; -import java.nio.file.Files; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); - private static final SessionManager SESSION_MANAGER = new SessionManager(); - private static final String REQUEST_LINE = "Request-Line"; - private static final String COOKIE = "Cookie"; - private static final String CONTENT_LENGTH = "Content-Length"; - private static final String TEXT_HTML = "text/html;"; - private static final String TEXT_CSS = "text/css;"; - private static final String INDEX_PAGE = "/index.html"; - private static final String NOT_FOUND_PAGE = "/404.html"; - private static final String HTML_EXTENSION = ".html"; - private static final String CONTENT_TYPE = "Content-Type"; - private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded"; private final Socket connection; @@ -48,15 +34,8 @@ public void process(final Socket connection) { try (final var inputStreamReader = new InputStreamReader(connection.getInputStream()); final var bufferedReader = new BufferedReader(inputStreamReader); final OutputStream outputStream = connection.getOutputStream()) { - final Map requestHeader = readRequestHeader(bufferedReader); - final String uri = requestHeader.get(REQUEST_LINE).split(" ")[1]; - final String contentType = requestHeader.get(CONTENT_TYPE); - - final String cookieHeader = requestHeader.getOrDefault(COOKIE, null); - final var cookie = HttpCookie.from(cookieHeader); - - final String requestBody = readRequestBody(bufferedReader, requestHeader, contentType); - final String response = handleRequest(uri, requestBody, cookie); + final var httpRequest = HttpRequest.from(bufferedReader); + final String response = handleRequest(httpRequest); outputStream.write(response.getBytes()); outputStream.flush(); @@ -65,165 +44,10 @@ public void process(final Socket connection) { } } - private String readRequestBody(final BufferedReader bufferedReader, final Map requestHeader, - final String contentType) throws IOException { - if (!Objects.equals(contentType, FORM_CONTENT_TYPE)) { - return null; - } - final var contentLength = Integer.parseInt(requestHeader.get(CONTENT_LENGTH)); - final var buffer = new char[contentLength]; - bufferedReader.read(buffer, 0, contentLength); - final var requestBody = new String(buffer); - - log.info("Request-Body: {}", requestBody); - return requestBody; - } - - private String handleRequest(final String uri, final String requestBody, final HttpCookie cookie) throws IOException { - final String path = uri.split("\\?")[0]; - - if (path.equals("/")) { - return get200ResponseMessage(path, "Hello world!"); - } - if (path.equals("/login")) { - final String responseBody = findStaticResource(path + HTML_EXTENSION); - return handleLoginRequest(uri, requestBody, responseBody, cookie); - } - if (path.equals("/register")) { - final String responseBody = findStaticResource(path + HTML_EXTENSION); - return handleRegisterRequest(uri, requestBody, responseBody, cookie); - } - final String responseBody = findStaticResource(path); - return get200ResponseMessage(path, responseBody); - } - - private String handleRegisterRequest(final String uri, final String requestBody, final String responseBody, - final HttpCookie cookie) { - final String[] splitUri = uri.split("\\?"); - if (requestBody == null && splitUri.length == 1) { - return get200ResponseMessage(splitUri[0], responseBody); - } - final Map requestBodyValues = getRequestParameters(requestBody, splitUri); - final var user = new User(requestBodyValues.get("account"), requestBodyValues.get("password"), - requestBodyValues.get("email")); - InMemoryUserRepository.save(user); - return get302ResponseMessage(INDEX_PAGE, cookie.getJSessionId(), false); - } - - private Map getRequestParameters(final String requestBody, final String[] uri) { - if (requestBody == null) { - return parseRequestBody(uri[1]); - } - return parseRequestBody(requestBody); - } - - private Map parseRequestBody(final String requestBody) { - final var requestBodyValues = new HashMap(); - final String[] splitRequestBody = requestBody.split("&"); - for (var value : splitRequestBody) { - final String[] splitValue = value.split("="); - requestBodyValues.put(splitValue[0], splitValue[1]); - } - return requestBodyValues; - } - - private String handleLoginRequest(final String uri, final String requestBody, final String responseBody, - final HttpCookie cookie) { - final String[] splitUri = uri.split("\\?"); - final User user = findUserBySessionId(cookie.getJSessionId()); - if (user != null) { - log.info("User: {}", user); - return get302ResponseMessage(INDEX_PAGE, cookie.getJSessionId(), false); - } - if (requestBody == null && splitUri.length == 1) { - return get200ResponseMessage(splitUri[0], responseBody); - } - return handleFirstLogin(requestBody, cookie, splitUri); - } - - private String handleFirstLogin(final String requestBody, final HttpCookie cookie, final String[] uri) { - final Map requestBodyValues = getRequestParameters(requestBody, uri); - final Optional user = InMemoryUserRepository.findByAccount(requestBodyValues.get("account")); - if (user.isEmpty() || !user.get().checkPassword(requestBodyValues.get("password"))) { - return get302ResponseMessage("/401.html", cookie.getJSessionId(), false); - } - final String sessionId = addSession(user.get()); - log.info("User: {}", user.get()); - return get302ResponseMessage(INDEX_PAGE, sessionId, true); - } - - private String addSession(final User user) { - final var session = Session.create(); - session.setAttribute("user", user); - SESSION_MANAGER.add(session); - return session.getId(); - } - - private User findUserBySessionId(final String sessionId) { - if (sessionId == null) { - return null; - } - final Session session = SESSION_MANAGER.findSession(sessionId) - .orElseGet(Session::create); - return (User) session.getAttribute("user"); - } - - private String get200ResponseMessage(final String path, final String responseBody) { - final String contentType = getContentType(path); - return String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: " + contentType + "charset=utf-8 ", - "Content-Length: " + responseBody.getBytes().length + " ", - "", - responseBody); - } - - private String getContentType(final String path) { - if (path.endsWith(".css")) { - return TEXT_CSS; - } - return TEXT_HTML; - } - - private String get302ResponseMessage(final String location, final String sessionId, final boolean setCookie) { - if (setCookie) { - return String.join("\r\n", - "HTTP/1.1 302 Found ", - "Location: " + location + " ", - "Set-Cookie: JSESSIONID=" + sessionId); - } - return String.join("\r\n", - "HTTP/1.1 302 Found ", - "Location: " + location); - } - - private String findStaticResource(final String uri) throws IOException { - final URL fileUrl = findResourceUrl(uri); - final String filePath = fileUrl.getPath(); - return Files.readString(new File(filePath).toPath()); - } - - private URL findResourceUrl(final String uri) { - final URL fileUrl = getClass().getClassLoader().getResource("./static" + uri); - if (fileUrl == null) { - return getClass().getClassLoader().getResource("./static" + NOT_FOUND_PAGE); - } - return fileUrl; - } - - private Map readRequestHeader(final BufferedReader bufferedReader) throws IOException { - final var requestHeader = new HashMap(); - - String line = bufferedReader.readLine(); - if (line == null) { - return Map.of(); - } - requestHeader.put(REQUEST_LINE, line); - - while (!"".equals(line = bufferedReader.readLine())) { - final String[] splitLine = line.split(": "); - requestHeader.put(splitLine[0], splitLine[1]); - } - return requestHeader; + private String handleRequest(final HttpRequest httpRequest) throws IOException { + final var httpResponse = HttpResponse.createEmpty(); + final var controller = RequestMapping.getController(httpRequest); + controller.service(httpRequest, httpResponse); + return httpResponse.toString(); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java b/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java new file mode 100644 index 0000000000..cf185c7b7a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11; + +import nextstep.jwp.controller.*; +import org.apache.coyote.http11.request.HttpRequest; + +import java.util.HashMap; +import java.util.Map; + +public class RequestMapping { + + private static final Map mappers = new HashMap<>(); + + static { + mappers.put("/", new HomeController()); + mappers.put("/login", new LoginController()); + mappers.put("/register", new RegisterController()); + } + + private RequestMapping() { + } + + public static Controller getController(final HttpRequest request) { + final String uri = request.getRequestLine().getPath(); + final String path = uri.split("\\?")[0]; + return mappers.getOrDefault(path, new ResourceController()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java index da868eec24..7516f736ce 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java @@ -2,13 +2,13 @@ import org.apache.catalina.Manager; -import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; public class SessionManager implements Manager { - private static final Map SESSIONS = new HashMap<>(); + private static final Map SESSIONS = new ConcurrentHashMap<>(); @Override public void add(final Session session) { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java new file mode 100644 index 0000000000..452dc83505 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java @@ -0,0 +1,5 @@ +package org.apache.coyote.http11.request; + +public enum HttpMethod { + POST, GET +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java new file mode 100644 index 0000000000..ab5269bdb9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,94 @@ +package org.apache.coyote.http11.request; + +import org.apache.coyote.http11.HttpCookie; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class HttpRequest { + + private static final String CONTENT_LENGTH = "Content-Length"; + + private final RequestLine requestLine; + private final RequestHeader requestHeader; + private final String requestBody; + + private HttpRequest(final RequestLine requestLine, final RequestHeader requestHeader, final String requestBody) { + this.requestLine = requestLine; + this.requestHeader = requestHeader; + this.requestBody = requestBody; + } + + public static HttpRequest from(final BufferedReader bufferedReader) throws IOException { + final RequestLine requestLine = readRequestLine(bufferedReader); + final RequestHeader requestHeader = readRequestHeader(bufferedReader); + final String requestBody = readRequestBody(bufferedReader, requestHeader); + return new HttpRequest(requestLine, requestHeader, requestBody); + } + + private static RequestLine readRequestLine(final BufferedReader bufferedReader) throws IOException { + final String line = bufferedReader.readLine(); + if (line == null) { + return null; + } + return RequestLine.from(line); + } + + private static RequestHeader readRequestHeader(final BufferedReader bufferedReader) throws IOException { + final var lines = new ArrayList(); + String line; + while (!"".equals(line = bufferedReader.readLine())) { + lines.add(line); + } + return RequestHeader.from(lines); + } + + private static String readRequestBody(final BufferedReader bufferedReader, final RequestHeader requestHeader) + throws IOException { + if (requestHeader.isNotFormContentType()) { + return null; + } + final var contentLength = Integer.parseInt(requestHeader.getValue(CONTENT_LENGTH)); + final var buffer = new char[contentLength]; + bufferedReader.read(buffer, 0, contentLength); + return new String(buffer); + } + + public Map getRequestParameters() { + final String[] uri = requestLine.getPath().split("\\?"); + if (requestBody == null) { + return parseRequestBody(uri[1]); + } + return parseRequestBody(requestBody); + } + + private Map parseRequestBody(final String requestBody) { + final var requestBodyValues = new HashMap(); + final String[] splitRequestBody = requestBody.split("&"); + for (var value : splitRequestBody) { + final String[] splitValue = value.split("="); + requestBodyValues.put(splitValue[0], splitValue[1]); + } + return requestBodyValues; + } + + public HttpCookie getCookie() { + final String cookieHeader = requestHeader.getValue("Cookie"); + return HttpCookie.from(cookieHeader); + } + + public RequestLine getRequestLine() { + return requestLine; + } + + public RequestHeader getRequestHeader() { + return requestHeader; + } + + public String getRequestBody() { + return requestBody; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java new file mode 100644 index 0000000000..fc06d17313 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java @@ -0,0 +1,33 @@ +package org.apache.coyote.http11.request; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class RequestHeader { + + private static final String CONTENT_TYPE = "Content-Type"; + private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded"; + + private final Map headers; + + private RequestHeader(final Map headers) { + this.headers = headers; + } + + public static RequestHeader from(final List lines) { + final Map headers = lines.stream() + .map(it -> it.split(": ")) + .collect(Collectors.toMap(it -> it[0], it -> it[1])); + return new RequestHeader(headers); + } + + public boolean isNotFormContentType() { + return !Objects.equals(this.getValue(CONTENT_TYPE), FORM_CONTENT_TYPE); + } + + public String getValue(final String key) { + return headers.getOrDefault(key, null); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java new file mode 100644 index 0000000000..81f814b049 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java @@ -0,0 +1,31 @@ +package org.apache.coyote.http11.request; + +public class RequestLine { + + private final HttpMethod method; + private final String path; + private final String protocol; + + private RequestLine(final HttpMethod method, final String path, final String protocol) { + this.method = method; + this.path = path; + this.protocol = protocol; + } + + public static RequestLine from(final String line) { + final String[] splitLine = line.split(" "); + return new RequestLine(HttpMethod.valueOf(splitLine[0]), splitLine[1], splitLine[2]); + } + + public String getMethod() { + return method.name(); + } + + public String getPath() { + return path; + } + + public String getProtocol() { + return protocol; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java new file mode 100644 index 0000000000..087bdc0472 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,57 @@ +package org.apache.coyote.http11.response; + +public class HttpResponse { + + private StatusLine statusLine; + private final ResponseHeader responseHeader; + private ResponseBody responseBody; + + private HttpResponse(final StatusLine statusLine, final ResponseHeader responseHeader, + final ResponseBody responseBody) { + this.statusLine = statusLine; + this.responseHeader = responseHeader; + this.responseBody = responseBody; + } + + public static HttpResponse createEmpty() { + return new HttpResponse(null, ResponseHeader.createEmpty(), null); + } + + public StatusLine getStatusLine() { + return statusLine; + } + + public ResponseHeader getResponseHeader() { + return responseHeader; + } + + public ResponseBody getResponseBody() { + return responseBody; + } + + public void setStatusLine(final StatusLine statusLine) { + this.statusLine = statusLine; + } + + public void addResponseHeader(final String key, final String value) { + this.responseHeader.addHeader(key, value); + } + + public void setResponseBody(final ResponseBody responseBody) { + this.responseBody = responseBody; + } + + @Override + public String toString() { + if (responseBody == null) { + return String.join("\r\n", + statusLine.toString(), + responseHeader.toString()); + } + return String.join("\r\n", + statusLine.toString(), + responseHeader.toString(), + "", + responseBody.getBody()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java new file mode 100644 index 0000000000..6f1b38d890 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java @@ -0,0 +1,25 @@ +package org.apache.coyote.http11.response; + +public enum HttpStatus { + + OK(200, "OK"), + FOUND(302, "Found"), + METHOD_NOT_ALLOWED(405, "Method Not Allowed"), + ; + + private final int code; + private final String message; + + HttpStatus(final int code, final String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseBody.java b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseBody.java new file mode 100644 index 0000000000..1ceb413326 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseBody.java @@ -0,0 +1,38 @@ +package org.apache.coyote.http11.response; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; + +public class ResponseBody { + + private final String body; + + private ResponseBody(final String body) { + this.body = body; + } + + public static ResponseBody fromUri(final String uri) throws IOException { + final URL fileUrl = findResourceUrl(uri); + final String filePath = fileUrl.getPath(); + final String body = Files.readString(new File(filePath).toPath()); + return new ResponseBody(body); + } + + public static ResponseBody fromText(final String text) { + return new ResponseBody(text); + } + + private static URL findResourceUrl(final String uri) { + final URL fileUrl = ResponseBody.class.getClassLoader().getResource("./static" + uri); + if (fileUrl == null) { + return ResponseBody.class.getClassLoader().getResource("./static/404.html"); + } + return fileUrl; + } + + public String getBody() { + return body; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseHeader.java new file mode 100644 index 0000000000..a8bf5ac103 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseHeader.java @@ -0,0 +1,35 @@ +package org.apache.coyote.http11.response; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class ResponseHeader { + + private final Map headers; + + private ResponseHeader(final Map headers) { + this.headers = headers; + } + + public static ResponseHeader createEmpty() { + return new ResponseHeader(new LinkedHashMap<>()); + } + + public void addHeader(final String key, final String value) { + headers.put(key, value); + } + + public Map getHeaders() { + return headers; + } + + @Override + public String toString() { + final StringBuilder stringBuilder = new StringBuilder(); + for (final var headerEntrySet : headers.entrySet()) { + stringBuilder.append(headerEntrySet.getKey()).append(": ").append(headerEntrySet.getValue()); + stringBuilder.append("\r\n"); + } + return stringBuilder.toString().trim(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/StatusLine.java b/tomcat/src/main/java/org/apache/coyote/http11/response/StatusLine.java new file mode 100644 index 0000000000..d07c6e5700 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/StatusLine.java @@ -0,0 +1,29 @@ +package org.apache.coyote.http11.response; + +public class StatusLine { + + private final String protocol; + private final HttpStatus httpStatus; + + private StatusLine(final String protocol, final HttpStatus httpStatus) { + this.protocol = protocol; + this.httpStatus = httpStatus; + } + + public static StatusLine of(final String protocol, final HttpStatus httpStatus) { + return new StatusLine(protocol, httpStatus); + } + + public String getProtocol() { + return protocol; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String toString() { + return String.join(" ", protocol, String.valueOf(httpStatus.getCode()), httpStatus.getMessage()); + } +} diff --git a/tomcat/src/test/java/nextstep/jwp/controller/HomeControllerTest.java b/tomcat/src/test/java/nextstep/jwp/controller/HomeControllerTest.java new file mode 100644 index 0000000000..0bff895422 --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/HomeControllerTest.java @@ -0,0 +1,96 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.*; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HomeControllerTest { + + @Test + void POST_요청이_들어오면_예외를_반환한다() throws IOException { + // given + final var homeController = new HomeController(); + final var requestLine = "POST / HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Content-Length: 58", + "Content-Type: application/x-www-form-urlencoded", + "Accept: */*"); + final var requestBody = "account=mango&password=password&email=mango%40woowahan.com"; + final var request = String.join("\r\n", requestLine, requestHeader, "", requestBody); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + final var httpRequest = HttpRequest.from(bufferedReader); + final var httpResponse = HttpResponse.createEmpty(); + + final var expectedStatusLine = StatusLine.of("HTTP/1.1", HttpStatus.METHOD_NOT_ALLOWED); + + // when + homeController.doPost(httpRequest, httpResponse); + + // then + assertThat(httpResponse.getStatusLine()) + .usingRecursiveComparison() + .isEqualTo(expectedStatusLine); + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } + + @Test + void GET_요청이_들어오면_HttpResponse에_응답을_처리한다() throws IOException { + // given + final var homeController = new HomeController(); + final var requestLine = "GET / HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Accept: */*", + ""); + final var request = String.join("\r\n", requestLine, requestHeader, ""); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + final var httpRequest = HttpRequest.from(bufferedReader); + final var httpResponse = HttpResponse.createEmpty(); + + final var expectedStatusLine = StatusLine.of("HTTP/1.1", HttpStatus.OK); + final var expectedResponseHeader = ResponseHeader.createEmpty(); + final var expectedResponseBody = ResponseBody.fromText("Hello world!"); + expectedResponseHeader.addHeader("Content-Type", "text/html;charset=utf-8"); + expectedResponseHeader.addHeader("Content-Length", String.valueOf(expectedResponseBody.getBody().getBytes().length)); + + // when + homeController.doGet(httpRequest, httpResponse); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(httpResponse.getStatusLine()) + .usingRecursiveComparison() + .isEqualTo(expectedStatusLine); + softAssertions.assertThat(httpResponse.getResponseHeader()) + .usingRecursiveComparison() + .isEqualTo(expectedResponseHeader); + softAssertions.assertThat(httpResponse.getResponseBody()) + .usingRecursiveComparison() + .isEqualTo(expectedResponseBody); + }); + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } +} diff --git a/tomcat/src/test/java/nextstep/jwp/controller/LoginControllerTest.java b/tomcat/src/test/java/nextstep/jwp/controller/LoginControllerTest.java new file mode 100644 index 0000000000..7fcd99d12f --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/LoginControllerTest.java @@ -0,0 +1,101 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.*; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class LoginControllerTest { + + @Test + void POST_요청이_들어오면_HttpResponse에_응답을_처리한다() throws IOException { + // given + final var loginController = new LoginController(); + final var requestLine = "POST /login HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Content-Length: 30", + "Content-Type: application/x-www-form-urlencoded", + "Accept: */*"); + final var requestBody = "account=gugu&password=password"; + final var request = String.join("\r\n", requestLine, requestHeader, "", requestBody); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + final var httpRequest = HttpRequest.from(bufferedReader); + final var httpResponse = HttpResponse.createEmpty(); + + final var expectedStatusLine = StatusLine.of("HTTP/1.1", HttpStatus.FOUND); + final var expectedResponseHeader = ResponseHeader.createEmpty(); + expectedResponseHeader.addHeader("Location", "/index.html"); + + // when + loginController.doPost(httpRequest, httpResponse); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(httpResponse.getStatusLine()) + .usingRecursiveComparison() + .isEqualTo(expectedStatusLine); + softAssertions.assertThat(httpResponse.getResponseHeader().getHeaders().get("Location")) + .isEqualTo(expectedResponseHeader.getHeaders().get("Location")); + }); + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } + + @Test + void GET_요청이_들어오면_HttpResponse에_응답을_처리한다() throws IOException { + // given + final var loginController = new LoginController(); + final var requestLine = "GET /login HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Accept: */*", + ""); + final var request = String.join("\r\n", requestLine, requestHeader, ""); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + final var httpRequest = HttpRequest.from(bufferedReader); + final var httpResponse = HttpResponse.createEmpty(); + + final var expectedStatusLine = StatusLine.of("HTTP/1.1", HttpStatus.OK); + final var expectedResponseHeader = ResponseHeader.createEmpty(); + final var expectedResponseBody = ResponseBody.fromUri("/login.html"); + expectedResponseHeader.addHeader("Content-Type", "text/html;charset=utf-8"); + expectedResponseHeader.addHeader("Content-Length", String.valueOf(expectedResponseBody.getBody().getBytes().length)); + + // when + loginController.doGet(httpRequest, httpResponse); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(httpResponse.getStatusLine()) + .usingRecursiveComparison() + .isEqualTo(expectedStatusLine); + softAssertions.assertThat(httpResponse.getResponseHeader()) + .usingRecursiveComparison() + .isEqualTo(expectedResponseHeader); + softAssertions.assertThat(httpResponse.getResponseBody()) + .usingRecursiveComparison() + .isEqualTo(expectedResponseBody); + }); + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } +} diff --git a/tomcat/src/test/java/nextstep/jwp/controller/RegisterControllerTest.java b/tomcat/src/test/java/nextstep/jwp/controller/RegisterControllerTest.java new file mode 100644 index 0000000000..99f5876d31 --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/RegisterControllerTest.java @@ -0,0 +1,102 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.*; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RegisterControllerTest { + + @Test + void POST_요청이_들어오면_HttpResponse에_응답을_처리한다() throws IOException { + // given + final var registerController = new RegisterController(); + final var requestLine = "POST /register HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Content-Length: 58", + "Content-Type: application/x-www-form-urlencoded", + "Accept: */*"); + final var requestBody = "account=mango&password=password&email=mango%40woowahan.com"; + final var request = String.join("\r\n", requestLine, requestHeader, "", requestBody); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + final var httpRequest = HttpRequest.from(bufferedReader); + final var httpResponse = HttpResponse.createEmpty(); + + final var expectedStatusLine = StatusLine.of("HTTP/1.1", HttpStatus.FOUND); + final var expectedResponseHeader = ResponseHeader.createEmpty(); + expectedResponseHeader.addHeader("Location", "/index.html"); + + // when + registerController.doPost(httpRequest, httpResponse); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(httpResponse.getStatusLine()) + .usingRecursiveComparison() + .isEqualTo(expectedStatusLine); + softAssertions.assertThat(httpResponse.getResponseHeader()) + .usingRecursiveComparison() + .isEqualTo(expectedResponseHeader); + }); + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } + + @Test + void GET_요청이_들어오면_HttpResponse에_응답을_처리한다() throws IOException { + // given + final var registerController = new RegisterController(); + final var requestLine = "GET /register HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Accept: */*", + ""); + final var request = String.join("\r\n", requestLine, requestHeader, ""); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + final var httpRequest = HttpRequest.from(bufferedReader); + final var httpResponse = HttpResponse.createEmpty(); + + final var expectedStatusLine = StatusLine.of("HTTP/1.1", HttpStatus.OK); + final var expectedResponseHeader = ResponseHeader.createEmpty(); + final var expectedResponseBody = ResponseBody.fromUri("/register.html"); + expectedResponseHeader.addHeader("Content-Type", "text/html;charset=utf-8"); + expectedResponseHeader.addHeader("Content-Length", String.valueOf(expectedResponseBody.getBody().getBytes().length)); + + // when + registerController.doGet(httpRequest, httpResponse); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(httpResponse.getStatusLine()) + .usingRecursiveComparison() + .isEqualTo(expectedStatusLine); + softAssertions.assertThat(httpResponse.getResponseHeader()) + .usingRecursiveComparison() + .isEqualTo(expectedResponseHeader); + softAssertions.assertThat(httpResponse.getResponseBody()) + .usingRecursiveComparison() + .isEqualTo(expectedResponseBody); + }); + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } +} diff --git a/tomcat/src/test/java/nextstep/jwp/controller/ResourceControllerTest.java b/tomcat/src/test/java/nextstep/jwp/controller/ResourceControllerTest.java new file mode 100644 index 0000000000..e1532ff5a0 --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/ResourceControllerTest.java @@ -0,0 +1,97 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.*; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ResourceControllerTest { + + @Test + void POST_요청이_들어오면_예외를_반환한다() throws IOException { + // given + final var resourceController = new ResourceController(); + final var requestLine = "POST /index.html HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Content-Length: 58", + "Content-Type: application/x-www-form-urlencoded", + "Accept: */*"); + final var requestBody = "account=mango&password=password&email=mango%40woowahan.com"; + final var request = String.join("\r\n", requestLine, requestHeader, "", requestBody); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + final var httpRequest = HttpRequest.from(bufferedReader); + final var httpResponse = HttpResponse.createEmpty(); + + final var expectedStatusLine = StatusLine.of("HTTP/1.1", HttpStatus.METHOD_NOT_ALLOWED); + + // when + resourceController.doPost(httpRequest, httpResponse); + + // then + assertThat(httpResponse.getStatusLine()) + .usingRecursiveComparison() + .isEqualTo(expectedStatusLine); + + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } + + @Test + void GET_요청이_들어오면_HttpResponse에_응답을_처리한다() throws IOException { + // given + final var resourceController = new ResourceController(); + final var requestLine = "GET /index.html HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Accept: */*", + ""); + final var request = String.join("\r\n", requestLine, requestHeader, ""); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + final var httpRequest = HttpRequest.from(bufferedReader); + final var httpResponse = HttpResponse.createEmpty(); + + final var expectedStatusLine = StatusLine.of("HTTP/1.1", HttpStatus.OK); + final var expectedResponseHeader = ResponseHeader.createEmpty(); + final var expectedResponseBody = ResponseBody.fromUri("/index.html"); + expectedResponseHeader.addHeader("Content-Type", "text/html;charset=utf-8"); + expectedResponseHeader.addHeader("Content-Length", String.valueOf(expectedResponseBody.getBody().getBytes().length)); + + // when + resourceController.doGet(httpRequest, httpResponse); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(httpResponse.getStatusLine()) + .usingRecursiveComparison() + .isEqualTo(expectedStatusLine); + softAssertions.assertThat(httpResponse.getResponseHeader()) + .usingRecursiveComparison() + .isEqualTo(expectedResponseHeader); + softAssertions.assertThat(httpResponse.getResponseBody()) + .usingRecursiveComparison() + .isEqualTo(expectedResponseBody); + }); + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } +} diff --git a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java index 512b919f09..833b6406cf 100644 --- a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java @@ -1,13 +1,14 @@ package nextstep.org.apache.coyote.http11; -import support.StubSocket; import org.apache.coyote.http11.Http11Processor; import org.junit.jupiter.api.Test; +import support.StubSocket; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; +import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; @@ -24,9 +25,9 @@ void process() { // then var expected = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: 12 ", + "HTTP/1.1 200 OK", + "Content-Type: text/html;charset=utf-8", + "Content-Length: 12", "", "Hello world!"); @@ -37,9 +38,9 @@ void process() { void index() throws IOException { // given final String httpRequest= String.join("\r\n", - "GET /index.html HTTP/1.1 ", - "Host: localhost:8080 ", - "Connection: keep-alive ", + "GET /index.html HTTP/1.1", + "Host: localhost:8080", + "Connection: keep-alive", "", ""); @@ -51,11 +52,11 @@ void index() throws IOException { // then final URL resource = getClass().getClassLoader().getResource("static/index.html"); - var expected = "HTTP/1.1 200 OK \r\n" + - "Content-Type: text/html;charset=utf-8 \r\n" + - "Content-Length: 5564 \r\n" + + var expected = "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/html;charset=utf-8\r\n" + + "Content-Length: 5564\r\n" + "\r\n"+ - new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + new String(Files.readAllBytes(new File(Objects.requireNonNull(resource).getFile()).toPath())); assertThat(socket.output()).isEqualTo(expected); } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/RequestMappingTest.java b/tomcat/src/test/java/org/apache/coyote/http11/RequestMappingTest.java new file mode 100644 index 0000000000..44f14ff7ac --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/RequestMappingTest.java @@ -0,0 +1,127 @@ +package org.apache.coyote.http11; + +import nextstep.jwp.controller.HomeController; +import nextstep.jwp.controller.LoginController; +import nextstep.jwp.controller.RegisterController; +import nextstep.jwp.controller.ResourceController; +import org.apache.coyote.http11.request.HttpRequest; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RequestMappingTest { + + @Nested + class getController_테스트 { + + @Test + void HomeController_객체를_반환한다() throws IOException { + // given + final var requestLine = "GET / HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Accept: */*", + ""); + final var request = String.join("\r\n", requestLine, requestHeader, ""); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + final var httpRequest = HttpRequest.from(bufferedReader); + + // when + final var controller = RequestMapping.getController(httpRequest); + + // then + assertThat(controller).isInstanceOf(HomeController.class); + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } + + @Test + void LoginController_객체를_반환한다() throws IOException { + // given + final var requestLine = "GET /login HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Accept: */*", + ""); + final var request = String.join("\r\n", requestLine, requestHeader, ""); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + final var httpRequest = HttpRequest.from(bufferedReader); + + // when + final var controller = RequestMapping.getController(httpRequest); + + // then + assertThat(controller).isInstanceOf(LoginController.class); + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } + + @Test + void RegisterController_객체를_반환한다() throws IOException { + // given + final var requestLine = "GET /register HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Accept: */*", + ""); + final var request = String.join("\r\n", requestLine, requestHeader, ""); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + final var httpRequest = HttpRequest.from(bufferedReader); + + // when + final var controller = RequestMapping.getController(httpRequest); + + // then + assertThat(controller).isInstanceOf(RegisterController.class); + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } + + @Test + void ResourceController_객체를_반환한다() throws IOException { + // given + final var requestLine = "GET /index.html HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Accept: */*", + ""); + final var request = String.join("\r\n", requestLine, requestHeader, ""); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + final var httpRequest = HttpRequest.from(bufferedReader); + + // when + final var controller = RequestMapping.getController(httpRequest); + + // then + assertThat(controller).isInstanceOf(ResourceController.class); + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java new file mode 100644 index 0000000000..1d131859a6 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java @@ -0,0 +1,59 @@ +package org.apache.coyote.http11.request; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.stream.Collectors; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpRequestTest { + + @Test + void BufferedReader_통한_입력_값으로_HttpRequest_객체를_생성한다() throws IOException { + // given + final var requestLine = "POST /register HTTP/1.1"; + final var requestHeader = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Content-Length: 58", + "Content-Type: application/x-www-form-urlencoded", + "Accept: */*"); + final var requestBody = "account=mango&password=password&email=mango%40woowahan.com"; + final var request = String.join("\r\n", requestLine, requestHeader, "", requestBody); + final var inputStream = new ByteArrayInputStream(request.getBytes()); + final var inputStreamReader = new InputStreamReader(inputStream); + final var bufferedReader = new BufferedReader(inputStreamReader); + + final var expectedRequestLine = RequestLine.from(requestLine); + final var splitRequestHeader = Arrays.stream(requestHeader.split("\r\n")) + .collect(Collectors.toList()); + final var expectedRequestHeader = RequestHeader.from(splitRequestHeader); + + // when + final var actual = HttpRequest.from(bufferedReader); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.getRequestLine()) + .usingRecursiveComparison() + .isEqualTo(expectedRequestLine); + softAssertions.assertThat(actual.getRequestHeader()) + .usingRecursiveComparison() + .isEqualTo(expectedRequestHeader); + softAssertions.assertThat(actual.getRequestBody()) + .isEqualTo(requestBody); + }); + bufferedReader.close(); + inputStreamReader.close(); + inputStream.close(); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestHeaderTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestHeaderTest.java new file mode 100644 index 0000000000..99c90e2634 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestHeaderTest.java @@ -0,0 +1,118 @@ +package org.apache.coyote.http11.request; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RequestHeaderTest { + + @Test + void 문자열_리스트를_받아_RequestHeader_객체를_생성한다() { + // given + final var line1 = "HOST: localhost:8080"; + final var line2 = "Connection-Length: 59"; + final var line3 = "Content-Type: application/x-www-form-urlencoded"; + final var line4 = "Accept: */*"; + final var lines = List.of(line1, line2, line3, line4); + + final var expectedHost = "localhost:8080"; + final var expectedConnectionLength = "59"; + final var expectedContentType = "application/x-www-form-urlencoded"; + final var expectedAccept = "*/*"; + + // when + final var actual = RequestHeader.from(lines); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.getValue("HOST")) + .isEqualTo(expectedHost); + softAssertions.assertThat(actual.getValue("Connection-Length")) + .isEqualTo(expectedConnectionLength); + softAssertions.assertThat(actual.getValue("Content-Type")) + .isEqualTo(expectedContentType); + softAssertions.assertThat(actual.getValue("Accept")) + .isEqualTo(expectedAccept); + }); + } + + @Nested + class isNotFormContentType_테스트 { + + @Test + void Content_Type이_Form_형식이_아니면_true를_반환한다() { + // given + final var line1 = "HOST: localhost:8080"; + final var line2 = "Connection-Length: 59"; + final var line3 = "Content-Type: text/html"; + final var line4 = "Accept: */*"; + final var lines = List.of(line1, line2, line3, line4); + final var requestHeader = RequestHeader.from(lines); + + // when + final var actual = requestHeader.isNotFormContentType(); + + // then + assertThat(actual).isTrue(); + } + + @Test + void Content_Type이_Form_형식이면_false를_반환한다() { + // given + final var line1 = "HOST: localhost:8080"; + final var line2 = "Connection-Length: 59"; + final var line3 = "Content-Type: application/x-www-form-urlencoded"; + final var line4 = "Accept: */*"; + final var lines = List.of(line1, line2, line3, line4); + final var requestHeader = RequestHeader.from(lines); + + // when + final var actual = requestHeader.isNotFormContentType(); + + // then + assertThat(actual).isFalse(); + } + } + + @Nested + class getValue_테스트 { + + @Test + void key를_통해_해당하는_header_값을_조회한다() { + // given + final var lines = Collections.singletonList("HOST: localhost:8080"); + final var requestHeader = RequestHeader.from(lines); + final var key = "HOST"; + final var expected = "localhost:8080"; + + // when + final var actual = requestHeader.getValue(key); + + // then + assertThat(actual).isEqualTo(expected); + } + + @Test + void key가_존재하지_않으면_null_값을_반환한다() { + // given + final var lines = Collections.singletonList("HOST: localhost:8080"); + final var requestHeader = RequestHeader.from(lines); + final var key = "Content-Type"; + + // when + final var actual = requestHeader.getValue(key); + + // then + assertThat(actual).isNull(); + } + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestLineTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestLineTest.java new file mode 100644 index 0000000000..68bea5e6f4 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestLineTest.java @@ -0,0 +1,34 @@ +package org.apache.coyote.http11.request; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RequestLineTest { + + @Test + void 문자열을_받아_RequestLine_객체를_생성한다() { + // given + final var line = "GET / HTTP/1.1"; + final var expectedMethod = "GET"; + final var expectedPath = "/"; + final var expectedProtocol = "HTTP/1.1"; + + // when + final var actual = RequestLine.from(line); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.getMethod()) + .isEqualTo(expectedMethod); + softAssertions.assertThat(actual.getPath()) + .isEqualTo(expectedPath); + softAssertions.assertThat(actual.getProtocol()) + .isEqualTo(expectedProtocol); + }); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseTest.java b/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseTest.java new file mode 100644 index 0000000000..82cc6b670c --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseTest.java @@ -0,0 +1,78 @@ +package org.apache.coyote.http11.response; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpResponseTest { + + @Test + void 비어있는_HttpResponse_객체를_생성한다() { + // given & when + final var actual = HttpResponse.createEmpty(); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.getStatusLine()) + .isNull(); + softAssertions.assertThat(actual.getResponseHeader().getHeaders()) + .isEmpty(); + softAssertions.assertThat(actual.getResponseBody()) + .isNull(); + }); + } + + @Nested + class toString_테스트 { + + @Test + void ResponseBody가_없는_경우의_HttpResponse_메세지를_출력한다() { + // given + final var httpResponse = HttpResponse.createEmpty(); + final var statusLine = StatusLine.of("HTTP/1.1", HttpStatus.FOUND); + httpResponse.setStatusLine(statusLine); + httpResponse.addResponseHeader("Location", "/index.html"); + + final var expected = String.join("\r\n", + "HTTP/1.1 302 Found", + "Location: /index.html"); + + // when + final var actual = httpResponse.toString(); + + // then + assertThat(actual).isEqualTo(expected); + } + + @Test + void ResponseBody가_있는_경우의_HttpResponse_메세지를_출력한다() { + // given + final var httpResponse = HttpResponse.createEmpty(); + final var statusLine = StatusLine.of("HTTP/1.1", HttpStatus.OK); + httpResponse.setStatusLine(statusLine); + httpResponse.addResponseHeader("Content-Type", "text/html"); + httpResponse.addResponseHeader("Content-Length", "12"); + final var responseBody = ResponseBody.fromText("Hello World!"); + httpResponse.setResponseBody(responseBody); + + final var expected = String.join("\r\n", + "HTTP/1.1 200 OK", + "Content-Type: text/html", + "Content-Length: 12", + "", + "Hello World!"); + + // when + final var actual = httpResponse.toString(); + + // then + assertThat(actual).isEqualTo(expected); + } + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/response/ResponseBodyTest.java b/tomcat/src/test/java/org/apache/coyote/http11/response/ResponseBodyTest.java new file mode 100644 index 0000000000..b041d257de --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/ResponseBodyTest.java @@ -0,0 +1,50 @@ +package org.apache.coyote.http11.response; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ResponseBodyTest { + + @Nested + class 객체_생성_테스트 { + + @Test + void URI로_ResponseBody_객체를_생성한다() throws IOException { + // given + final var uri = "/index.html"; + + final var fileUrl = ResponseBody.class.getClassLoader().getResource("./static" + uri); + final var filePath = Objects.requireNonNull(fileUrl).getPath(); + final var expected = Files.readString(new File(filePath).toPath()); + + // when + final var actual = ResponseBody.fromUri(uri); + + // then + assertThat(actual.getBody()).isEqualTo(expected); + } + + @Test + void text로_ResponseBody_객체를_생성한다() { + // given + final var text = "Hello World!"; + + // when + final var actual = ResponseBody.fromText(text); + + // then + assertThat(actual.getBody()).isEqualTo(text); + } + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/response/ResponseHeaderTest.java b/tomcat/src/test/java/org/apache/coyote/http11/response/ResponseHeaderTest.java new file mode 100644 index 0000000000..6399fc5821 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/ResponseHeaderTest.java @@ -0,0 +1,60 @@ +package org.apache.coyote.http11.response; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ResponseHeaderTest { + + @Test + void 비어있는_ResponseHeader_객체를_생성한다() { + // given & when + final var actual = ResponseHeader.createEmpty(); + + // then + assertThat(actual.getHeaders()).isEmpty(); + } + + @Test + void 특정_header를_추가한다() { + // given + final var key = "Content-Type"; + final var value = "text/html"; + final var responseHeader = ResponseHeader.createEmpty(); + + // when + responseHeader.addHeader(key, value); + + // then + assertSoftly(softAssertions -> { + final var headers = responseHeader.getHeaders(); + softAssertions.assertThat(headers.containsKey(key)) + .isTrue(); + softAssertions.assertThat(headers.get(key)) + .isEqualTo(value); + }); + } + + @Test + void ResponseHeader_객체를_메세지로_출력한다() { + // given + final var responseHeader = ResponseHeader.createEmpty(); + responseHeader.addHeader("Content-Type", "text/html"); + responseHeader.addHeader("Content-Length", "80"); + + final var expected = String.join("\r\n", + "Content-Type: text/html", + "Content-Length: 80"); + + // when + final var actual = responseHeader.toString(); + + // then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/response/StatusLineTest.java b/tomcat/src/test/java/org/apache/coyote/http11/response/StatusLineTest.java new file mode 100644 index 0000000000..67388910df --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/StatusLineTest.java @@ -0,0 +1,47 @@ +package org.apache.coyote.http11.response; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class StatusLineTest { + + @Test + void StatusLine_객체를_생성한다() { + // given + final var protocol = "HTTP/1.1"; + final var httpStatus = HttpStatus.OK; + + // when + final var actual = StatusLine.of(protocol, httpStatus); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.getProtocol()) + .isEqualTo(protocol); + softAssertions.assertThat(actual.getHttpStatus()) + .isEqualTo(httpStatus); + }); + } + + @Test + void StatusLine_객체를_메세지로_출력한다() { + // given + final var protocol = "HTTP/1.1"; + final var httpStatus = HttpStatus.OK; + final var statusLine = StatusLine.of(protocol, httpStatus); + + final var expected = "HTTP/1.1 200 OK"; + + // when + final var actual = statusLine.toString(); + + // then + assertThat(actual).isEqualTo(expected); + } +}