From 187f9c280aedb41dcec6827cbec42a8893f72b7a Mon Sep 17 00:00:00 2001 From: Herb Date: Mon, 4 Sep 2023 19:38:13 +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-=201,=202=EB=8B=A8=EA=B3=84]=20=ED=97=88?= =?UTF-8?q?=EB=B8=8C(=EB=B0=A9=EB=8C=80=EC=9D=98)=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(#302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: step1을 위한 학습 테스트 추가 * feat: 정적 리소스에 대한 응답 기능 추가 * feat: css 지원 기능 추가 * feat: Query String 파싱 기능 구현 * docs: 요구사항 추가 * refactor: QueryString을 위한 Util 클래스 제거 및 QueryString 일급 컬렉션 추가 * feat: 빈 favicon 추가 * feat: Http 요청의 첫 라인을 의미하는 RequestLine, HttpMethod enum 추가 * feat: RequestLine 클래스 queryString 파싱하는 기능 추가 * refactor: process 메서드 flow 개선 * refactor: 기존의 util 클래스 제거하고 HttpResponseGenerator 추가 * feat: RequestHeader 클래스 추가 * feat: RequestBody 클래스 추가 * feat: Found Redirect 기능 추가 * feat: 회원가입 기능 추가 * refactor: 패키지 분리 * feat: 쿠키값을 가지고 있는 httpCookie 추가 * feat: 쿠키 세팅하는 기능 추가 * feat: Session, SessionManager 추가 * feat: 로그인시 세션에 사용자 저장 기능 추가 * refactor: 불필요한 static 메서드 제거 * fix: 자동 정렬된 index.html 수정 * fix: 코드 스멜 제거 * refactor: JSESSIONID, account, passoword 상수화 * refactor: final 빠진 부분 추가 * refactor: 외부에서 사용하지 않는 생성자 private으로 변경 * feat: HttpExtensionType Enum 추가 --- README.md | 16 ++ study/src/test/java/study/FileTest.java | 50 ++-- study/src/test/java/study/IOStreamTest.java | 133 +++++----- .../apache/catalina/connector/Connector.java | 7 +- .../apache/coyote/http11/Http11Processor.java | 142 +++++++++-- .../coyote/http11/common/Constants.java | 10 + .../coyote/http11/common/HttpCookie.java | 54 +++++ .../http11/common/HttpExtensionType.java | 34 +++ .../coyote/http11/common/HttpMethod.java | 16 ++ .../coyote/http11/common/HttpStatus.java | 18 ++ .../apache/coyote/http11/common/Session.java | 38 +++ .../coyote/http11/common/SessionManager.java | 21 ++ .../http11/exception/Http11Exception.java | 8 + .../exception/InvalidHttpMethodException.java | 8 + .../InvalidRequestLineException.java | 8 + .../coyote/http11/request/QueryString.java | 50 ++++ .../coyote/http11/request/RequestBody.java | 49 ++++ .../coyote/http11/request/RequestHeader.java | 46 ++++ .../coyote/http11/request/RequestLine.java | 65 +++++ .../response/HttpResponseGenerator.java | 76 ++++++ .../http11/response/ResponseEntity.java | 48 ++++ tomcat/src/main/resources/static/409.html | 52 ++++ tomcat/src/main/resources/static/favicon.ico | 0 tomcat/src/main/resources/static/login.html | 114 ++++----- .../coyote/http11/Http11ProcessorTest.java | 62 ----- .../coyote/http11/Http11ProcessorTest.java | 229 ++++++++++++++++++ .../coyote/http11/common/ContentTypeTest.java | 36 +++ .../coyote/http11/common/HttpCookieTest.java | 63 +++++ .../coyote/http11/common/HttpMethodTest.java | 24 ++ .../http11/common/SessionManagerTest.java | 52 ++++ .../coyote/http11/common/SessionTest.java | 61 +++++ .../http11/request/QueryStringTest.java | 28 +++ .../http11/request/RequestBodyTest.java | 46 ++++ .../http11/request/RequestHeaderTest.java | 46 ++++ .../http11/request/RequestLineTest.java | 73 ++++++ .../response/HttpResponseGeneratorTest.java | 78 ++++++ 36 files changed, 1638 insertions(+), 223 deletions(-) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/common/Constants.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/common/HttpExtensionType.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/common/Session.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/common/SessionManager.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/exception/Http11Exception.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidHttpMethodException.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidRequestLineException.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/QueryString.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.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/HttpResponseGenerator.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/response/ResponseEntity.java create mode 100644 tomcat/src/main/resources/static/409.html create mode 100644 tomcat/src/main/resources/static/favicon.ico delete mode 100644 tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/common/ContentTypeTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/common/HttpCookieTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/common/HttpMethodTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/common/SessionManagerTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/common/SessionTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/request/QueryStringTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/request/RequestBodyTest.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/HttpResponseGeneratorTest.java diff --git a/README.md b/README.md index b24f542e33..70be385f28 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ # 톰캣 구현하기 + +- [x] GET /index.html 요청에 응답한다. +- [x] CSS 형식의 정적 파일 요청도 지원한다. +- [x] QueryString 파싱 기능을 추가한다. +- [x] 로그인 기능(/login)을 구현한다. + - [x] 로그인의 성공하면 http status 302를 반환한다. + - [x] 로그인에 성공하면 세션에 사용자를 저장한다. + - [x] 로그인에 성공하면 JSESSIONID를 Set-Cookie 헤더에 담아 보낸다. + - [x] 로그인에 성공한 상태에서 로그인 페이지로 접근하면 index.html로 리다이렉트한다. + - [x] location header에 리다이렉트할 url을 추가한다. + - [x] 로그인에 실패하는 경우 401.html로 리다이렉트한다. +- [x] 회원가입 기능(/register)을 구현한다. + - [x] 회원가입 페이지의 경우 GET을 사용하여 보여준다. + - [x] 회원가입의 경우 POST를 사용한다. + - [x] 회원가입을 완료하는 경우 index.html로 리다이렉트한다. + diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..a0fbb4ca96 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,53 +1,57 @@ package study; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; -import java.nio.file.Path; -import java.util.Collections; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; /** - * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. - * File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다. + * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다. */ +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @DisplayName("File 클래스 학습 테스트") class FileTest { /** * resource 디렉터리 경로 찾기 - * - * File 객체를 생성하려면 파일의 경로를 알아야 한다. - * 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. - * resource 디렉터리의 경로는 어떻게 알아낼 수 있을까? + *

+ * File 객체를 생성하려면 파일의 경로를 알아야 한다. 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. resource 디렉터리의 경로는 어떻게 알아낼 수 + * 있을까? */ @Test void resource_디렉터리에_있는_파일의_경로를_찾는다() { final String fileName = "nextstep.txt"; + final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + final URL resource = classLoader.getResource(fileName); - // todo - final String actual = ""; + final String actual = resource.getFile(); assertThat(actual).endsWith(fileName); } /** * 파일 내용 읽기 - * - * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. - * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. + *

+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException { final String fileName = "nextstep.txt"; + ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + URL resource = classLoader.getResource(fileName); + String filePath = resource.getFile(); + File file = new File(filePath); - // todo - final Path path = null; - - // todo - final List actual = Collections.emptyList(); + final List actual = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8); assertThat(actual).containsOnly("nextstep"); } diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..cce5dca724 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,45 +1,54 @@ package study; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; import org.junit.jupiter.api.DisplayName; +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.*; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - /** - * 자바는 스트림(Stream)으로부터 I/O를 사용한다. - * 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. - * - * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. - * FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. - * FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환) - * - * Stream은 데이터를 바이트로 읽고 쓴다. - * 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. - * Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다. + * 자바는 스트림(Stream)으로부터 I/O를 사용한다. 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. + *

+ * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. FilterStream은 읽거나 쓰는 + * 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환) + *

+ * Stream은 데이터를 바이트로 읽고 쓴다. 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 + * 처리할 수 있다. */ +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @DisplayName("Java I/O Stream 클래스 학습 테스트") class IOStreamTest { /** * OutputStream 학습하기 - * - * 자바의 기본 출력 클래스는 java.io.OutputStream이다. - * OutputStream의 write(int b) 메서드는 기반 메서드이다. + *

+ * 자바의 기본 출력 클래스는 java.io.OutputStream이다. OutputStream의 write(int b) 메서드는 기반 메서드이다. * public abstract void write(int b) throws IOException; */ @Nested class OutputStream_학습_테스트 { /** - * OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다. - * OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다. - * 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, - * 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다. - * + * OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다. OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 + * 사용한다. 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 + * 사용한다. + *

* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다. * write(byte[] data)write(byte b[], int off, int len) 메서드는 * 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다. @@ -48,12 +57,13 @@ class OutputStream_학습_테스트 { void OutputStream은_데이터를_바이트로_처리한다() throws IOException { final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112}; final OutputStream outputStream = new ByteArrayOutputStream(bytes.length); + final String input = "nextstep"; /** * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ - + outputStream.write(input.getBytes(), 0, input.length()); final String actual = outputStream.toString(); assertThat(actual).isEqualTo("nextstep"); @@ -61,13 +71,10 @@ class OutputStream_학습_테스트 { } /** - * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. - * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. - * - * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. - * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. - * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 - * 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다. + * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. + *

+ * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. Stream은 + * 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다. */ @Test void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException { @@ -78,24 +85,25 @@ class OutputStream_학습_테스트 { * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException { final OutputStream outputStream = mock(OutputStream.class); - /** * todo * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (outputStream) { + } verify(outputStream, atLeastOnce()).close(); } @@ -103,20 +111,18 @@ class OutputStream_학습_테스트 { /** * InputStream 학습하기 - * - * 자바의 기본 입력 클래스는 java.io.InputStream이다. - * InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. - * InputStream의 read() 메서드는 기반 메서드이다. + *

+ * 자바의 기본 입력 클래스는 java.io.InputStream이다. InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. InputStream의 read() 메서드는 기반 + * 메서드이다. * public abstract int read() throws IOException; - * + *

* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다. */ @Nested class InputStream_학습_테스트 { /** - * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다. - * int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다. + * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다. int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다. * 그리고 Stream 끝에 도달하면 -1을 반환한다. */ @Test @@ -128,7 +134,7 @@ class InputStream_학습_테스트 { * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + final String actual = new String(inputStream.readAllBytes()); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -136,8 +142,7 @@ class InputStream_학습_테스트 { } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException { @@ -148,6 +153,8 @@ class InputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (inputStream) { + } verify(inputStream, atLeastOnce()).close(); } @@ -155,26 +162,24 @@ class InputStream_학습_테스트 { /** * FilterStream 학습하기 - * - * 필터는 필터 스트림, reader, writer로 나뉜다. - * 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. - * reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다. + *

+ * 필터는 필터 스트림, reader, writer로 나뉜다. 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 + * 텍스트를 처리하는 데 사용된다. */ @Nested class FilterStream_학습_테스트 { /** - * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. - * InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. - * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? + * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. 버퍼 크기를 지정하지 + * 않으면 버퍼의 기본 사이즈는 얼마일까? -> 8192 */ @Test - void 필터인_BufferedInputStream를_사용해보자() { + void 필터인_BufferedInputStream를_사용해보자() throws IOException { final String text = "필터에 연결해보자."; final InputStream inputStream = new ByteArrayInputStream(text.getBytes()); - final InputStream bufferedInputStream = null; + final InputStream bufferedInputStream = new BufferedInputStream(inputStream); - final byte[] actual = new byte[0]; + final byte[] actual = bufferedInputStream.readAllBytes(); assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); @@ -182,30 +187,32 @@ class FilterStream_학습_테스트 { } /** - * 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다. - * 문자열이 아닌 바이트 단위로 처리하려니 불편하다. - * 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다. - * reader, writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다. - * 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다. + * 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다. 문자열이 아닌 바이트 단위로 처리하려니 불편하다. 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다. reader, + * writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다. 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다. */ @Nested class InputStreamReader_학습_테스트 { /** - * InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다. - * 읽어온 문자(char)를 문자열(String)로 처리하자. - * 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. + * InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다. 읽어온 문자(char)를 문자열(String)로 처리하자. 필터인 BufferedReader를 사용하면 + * readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. */ @Test - void BufferedReader를_사용하여_문자열을_읽어온다() { + void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException { final String emoji = String.join("\r\n", "😀😃😄😁😆😅😂🤣🥲☺️😊", "😇🙂🙃😉😌😍🥰😘😗😙😚", "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); final StringBuilder actual = new StringBuilder(); + String line = bufferedReader.readLine(); + while (line != null) { + actual.append(line + "\r\n"); + line = bufferedReader.readLine(); + } assertThat(actual).hasToString(emoji); } 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..d171bb84a8 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -1,13 +1,12 @@ package org.apache.catalina.connector; -import org.apache.coyote.http11.Http11Processor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import org.apache.coyote.http11.Http11Processor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Connector implements Runnable { 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 7f1b2c7e96..71a73715f1 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,18 +1,44 @@ package org.apache.coyote.http11; +import static org.apache.coyote.http11.common.Constants.CRLF; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.util.UUID; +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.common.HttpCookie; +import org.apache.coyote.http11.common.HttpMethod; +import org.apache.coyote.http11.common.HttpStatus; +import org.apache.coyote.http11.common.Session; +import org.apache.coyote.http11.common.SessionManager; +import org.apache.coyote.http11.request.RequestBody; +import org.apache.coyote.http11.request.RequestHeader; +import org.apache.coyote.http11.request.RequestLine; +import org.apache.coyote.http11.response.HttpResponseGenerator; +import org.apache.coyote.http11.response.ResponseEntity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.Socket; - public class Http11Processor implements Runnable, Processor { - private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private static final Logger LOG = LoggerFactory.getLogger(Http11Processor.class); + private static final String INDEX_PAGE = "/index.html"; + private static final String REGISTER_PAGE = "/register.html"; + private static final String LOGIN_PAGE = "/login.html"; + private static final String ACCOUNT = "account"; + private static final String PASSWORD = "password"; + private static final String EMAIL = "email"; private final Socket connection; + private final HttpResponseGenerator httpResponseGenerator = new HttpResponseGenerator(); + private final SessionManager sessionManager = new SessionManager(); public Http11Processor(final Socket connection) { this.connection = connection; @@ -20,28 +46,112 @@ public Http11Processor(final Socket connection) { @Override public void run() { - log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); + LOG.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); process(connection); } @Override public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { - - final var responseBody = "Hello world!"; + try (final InputStream inputStream = connection.getInputStream(); + final OutputStream outputStream = connection.getOutputStream(); + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { + final String firstLine = bufferedReader.readLine(); + if (firstLine == null) { + return; + } + final RequestLine requestLine = RequestLine.from(firstLine); + final RequestHeader requestHeader = readHeader(bufferedReader); + final RequestBody requestBody = readBody(bufferedReader, requestHeader); - final var response = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: " + responseBody.getBytes().length + " ", - "", - responseBody); + final ResponseEntity responseEntity = handleRequest(requestLine, requestHeader, requestBody); + final String response = httpResponseGenerator.generate(responseEntity); outputStream.write(response.getBytes()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { - log.error(e.getMessage(), e); + LOG.error(e.getMessage(), e); + } + } + + private RequestHeader readHeader(final BufferedReader bufferedReader) throws IOException { + final StringBuilder stringBuilder = new StringBuilder(); + for (String line = bufferedReader.readLine(); !"".equals(line); line = bufferedReader.readLine()) { + stringBuilder.append(line).append(CRLF); + } + return RequestHeader.from(stringBuilder.toString()); + } + + private RequestBody readBody(final BufferedReader bufferedReader, final RequestHeader requestHeader) + throws IOException { + final String contentLength = requestHeader.get("Content-Length"); + if (contentLength == null) { + return RequestBody.empty(); + } + final int length = Integer.parseInt(contentLength); + char[] buffer = new char[length]; + bufferedReader.read(buffer, 0, length); + return RequestBody.from(new String(buffer)); + } + + private ResponseEntity handleRequest( + final RequestLine requestLine, + final RequestHeader requestHeader, + final RequestBody requestBody + ) { + final String path = requestLine.parseUriWithOutQueryString(); + if (path.equals("/login")) { + return login(requestLine, requestHeader, requestBody); + } + if (path.equals("/register")) { + return register(requestLine, requestBody); + } + return new ResponseEntity(HttpStatus.OK, path); + } + + private ResponseEntity login( + final RequestLine requestLine, + final RequestHeader requestHeader, + final RequestBody requestBody + ) { + if (requestLine.getHttpMethod() == HttpMethod.GET) { + final HttpCookie httpCookie = requestHeader.parseCookie(); + final Session session = sessionManager.findSession(httpCookie.getJSessionId()); + if (session != null) { + return new ResponseEntity(HttpStatus.FOUND, INDEX_PAGE); + } + return new ResponseEntity(HttpStatus.OK, LOGIN_PAGE); + } + final String account = requestBody.get(ACCOUNT); + final String password = requestBody.get(PASSWORD); + return InMemoryUserRepository.findByAccount(account) + .filter(user -> user.checkPassword(password)) + .map(this::loginSuccess) + .orElseGet(() -> new ResponseEntity(HttpStatus.UNAUTHORIZED, "/401.html")); + } + + private ResponseEntity loginSuccess(final User user) { + final String uuid = UUID.randomUUID().toString(); + final ResponseEntity responseEntity = new ResponseEntity(HttpStatus.FOUND, INDEX_PAGE); + responseEntity.setJSessionId(uuid); + final Session session = new Session(uuid); + session.setAttribute("user", user); + sessionManager.add(session); + return responseEntity; + } + + private ResponseEntity register(final RequestLine requestLine, final RequestBody requestBody) { + if (requestLine.getHttpMethod() == HttpMethod.GET) { + return new ResponseEntity(HttpStatus.OK, REGISTER_PAGE); } + final String account = requestBody.get(ACCOUNT); + + if (InMemoryUserRepository.findByAccount(account).isPresent()) { + return new ResponseEntity(HttpStatus.CONFLICT, "/409.html"); + } + + final String password = requestBody.get(PASSWORD); + final String email = requestBody.get(EMAIL); + InMemoryUserRepository.save(new User(account, password, email)); + return new ResponseEntity(HttpStatus.FOUND, INDEX_PAGE); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/Constants.java b/tomcat/src/main/java/org/apache/coyote/http11/common/Constants.java new file mode 100644 index 0000000000..be6350538c --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/Constants.java @@ -0,0 +1,10 @@ +package org.apache.coyote.http11.common; + +public class Constants { + + public static final String CRLF = "\r\n"; + public static final String BLANK = " "; + + private Constants() { + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java new file mode 100644 index 0000000000..779c9270c5 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java @@ -0,0 +1,54 @@ +package org.apache.coyote.http11.common; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toMap; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class HttpCookie { + + private static final String SEPARATOR = "; "; + private static final String DELIMITER = "="; + private static final String JSESSION_ID = "JSESSIONID"; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + + private final Map items = new HashMap<>(); + + public HttpCookie() { + } + + private HttpCookie(final Map items) { + this.items.putAll(items); + } + + public static HttpCookie from(final String cookie) { + if (cookie.isEmpty()) { + return new HttpCookie(); + } + return Arrays.stream(cookie.split(SEPARATOR)) + .map(header -> header.split(DELIMITER)) + .collect(collectingAndThen( + toMap(header -> header[KEY_INDEX], header -> header[VALUE_INDEX]), + HttpCookie::new + )); + } + + public void put(final String key, final String value) { + items.put(key, value); + } + + public String get(final String key) { + return items.get(key); + } + + public String getJSessionId() { + return items.get(JSESSION_ID); + } + + public Map getItems() { + return items; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpExtensionType.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpExtensionType.java new file mode 100644 index 0000000000..b815f235e9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpExtensionType.java @@ -0,0 +1,34 @@ +package org.apache.coyote.http11.common; + +import java.util.Arrays; + +public enum HttpExtensionType { + HTML(".html", "text/html"), + CSS(".css", "text/css"), + JS(".js", "text/javascript"), + ICO(".ico", "image/svg+xml"), + ; + + private final String extension; + private final String contentType; + + HttpExtensionType(final String extension, final String contentType) { + this.extension = extension; + this.contentType = contentType; + } + + public static HttpExtensionType from(final String file) { + return Arrays.stream(values()) + .filter(type -> file.endsWith(type.extension)) + .findAny() + .orElse(HTML); + } + + public String getExtension() { + return extension; + } + + public String getContentType() { + return contentType; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java new file mode 100644 index 0000000000..e57a21e81c --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java @@ -0,0 +1,16 @@ +package org.apache.coyote.http11.common; + +import java.util.Arrays; +import org.apache.coyote.http11.exception.InvalidHttpMethodException; + +public enum HttpMethod { + GET, + POST; + + public static HttpMethod from(final String input) { + return Arrays.stream(values()) + .filter(value -> value.name().equals(input)) + .findAny() + .orElseThrow(InvalidHttpMethodException::new); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java new file mode 100644 index 0000000000..cabc6af05f --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java @@ -0,0 +1,18 @@ +package org.apache.coyote.http11.common; + +public enum HttpStatus { + OK("200"), + FOUND("302"), + UNAUTHORIZED("401"), + CONFLICT("409"); + + private final String code; + + HttpStatus(final String code) { + this.code = code; + } + + public String getCode() { + return code; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/common/Session.java new file mode 100644 index 0000000000..1cb2c12f7e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/Session.java @@ -0,0 +1,38 @@ +package org.apache.coyote.http11.common; + +import java.util.HashMap; +import java.util.Map; + +public class Session { + + private final String id; + private final Map items = new HashMap<>(); + + public Session(final String id) { + this.id = id; + } + + public Object getAttribute(final String key) { + return items.get(key); + } + + public void setAttribute(final String key, final Object value) { + items.put(key, value); + } + + public void removeAttribute(final String key) { + items.remove(key); + } + + public void invalidate() { + items.clear(); + } + + public String getId() { + return id; + } + + public Map getItems() { + return items; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/common/SessionManager.java new file mode 100644 index 0000000000..08d276e37b --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/SessionManager.java @@ -0,0 +1,21 @@ +package org.apache.coyote.http11.common; + +import java.util.HashMap; +import java.util.Map; + +public class SessionManager { + + private static final Map SESSIONS = new HashMap<>(); + + public void add(final Session session) { + SESSIONS.put(session.getId(), session); + } + + public Session findSession(final String id) { + return SESSIONS.get(id); + } + + public void remove(final String id) { + SESSIONS.remove(id); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/Http11Exception.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/Http11Exception.java new file mode 100644 index 0000000000..1339c38abf --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/Http11Exception.java @@ -0,0 +1,8 @@ +package org.apache.coyote.http11.exception; + +public class Http11Exception extends RuntimeException { + + public Http11Exception(final String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidHttpMethodException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidHttpMethodException.java new file mode 100644 index 0000000000..c251e74e2b --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidHttpMethodException.java @@ -0,0 +1,8 @@ +package org.apache.coyote.http11.exception; + +public class InvalidHttpMethodException extends Http11Exception { + + public InvalidHttpMethodException() { + super("올바르지 않은 HttpMethod 형식입니다."); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidRequestLineException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidRequestLineException.java new file mode 100644 index 0000000000..5b30a82b9f --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidRequestLineException.java @@ -0,0 +1,8 @@ +package org.apache.coyote.http11.exception; + +public class InvalidRequestLineException extends Http11Exception { + + public InvalidRequestLineException() { + super("올바르지 않은 RequestLine 형식입니다."); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/QueryString.java b/tomcat/src/main/java/org/apache/coyote/http11/request/QueryString.java new file mode 100644 index 0000000000..ad50c1713a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/QueryString.java @@ -0,0 +1,50 @@ +package org.apache.coyote.http11.request; + +import static java.util.stream.Collectors.toMap; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class QueryString { + + private static final String QUERY_STRING_BEGIN = "?"; + private static final String SEPARATOR = "&"; + private static final String DELIMITER = "="; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + private static final int EMPTY = -1; + + private final Map items = new HashMap<>(); + + private QueryString() { + this(Map.of()); + } + + public QueryString(final Map items) { + this.items.putAll(items); + } + + public static QueryString from(final String uri) { + int queryStringIndex = uri.indexOf(QUERY_STRING_BEGIN); + if (queryStringIndex == EMPTY) { + return new QueryString(); + } + return new QueryString(parseQueryString(uri, queryStringIndex)); + } + + private static Map parseQueryString(final String uri, final int queryStringIndex) { + final String queryString = uri.substring(queryStringIndex + 1); + return Arrays.stream(queryString.split(SEPARATOR)) + .map(query -> query.split(DELIMITER)) + .collect(toMap(query -> query[KEY_INDEX], query -> query[VALUE_INDEX])); + } + + public String get(final String key) { + return items.get(key); + } + + public Map getItems() { + return items; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java new file mode 100644 index 0000000000..ec779872aa --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java @@ -0,0 +1,49 @@ +package org.apache.coyote.http11.request; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toMap; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class RequestBody { + + private static final String SEPARATOR = "&"; + private static final String DELIMITER = "="; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + + private final Map items = new HashMap<>(); + + private RequestBody() { + } + + private RequestBody(final Map items) { + this.items.putAll(items); + } + + public static RequestBody empty() { + return new RequestBody(); + } + + public static RequestBody from(final String body) { + if (body.isEmpty()) { + return new RequestBody(); + } + return Arrays.stream(body.split(SEPARATOR)) + .map(field -> field.split(DELIMITER)) + .collect(collectingAndThen( + toMap(field -> field[KEY_INDEX], field -> field[VALUE_INDEX]), + RequestBody::new + )); + } + + public String get(final String key) { + return items.get(key); + } + + public Map getItems() { + return items; + } +} 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..44f4c78c71 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java @@ -0,0 +1,46 @@ +package org.apache.coyote.http11.request; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toMap; +import static org.apache.coyote.http11.common.Constants.CRLF; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.apache.coyote.http11.common.HttpCookie; + +public class RequestHeader { + + private static final String DELIMITER = ": "; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + private static final String COOKIE_HEADER = "Cookie"; + + private final Map items = new HashMap<>(); + + private RequestHeader(final Map items) { + this.items.putAll(items); + } + + public static RequestHeader from(final String headers) { + return Arrays.stream(headers.split(CRLF)) + .map(header -> header.split(DELIMITER)) + .collect(collectingAndThen( + toMap(header -> header[KEY_INDEX], header -> header[VALUE_INDEX]), + RequestHeader::new + )); + } + + public HttpCookie parseCookie() { + final String cookie = items.getOrDefault(COOKIE_HEADER, ""); + return HttpCookie.from(cookie); + } + + public String get(final String key) { + return items.get(key); + } + + public Map getItems() { + return items; + } +} 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..0e0b152270 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java @@ -0,0 +1,65 @@ +package org.apache.coyote.http11.request; + +import org.apache.coyote.http11.common.HttpMethod; +import org.apache.coyote.http11.exception.InvalidRequestLineException; + +public class RequestLine { + + private static final int HTTP_METHOD_INDEX = 0; + private static final int URI_INDEX = 1; + private static final int HTTP_VERSION_INDEX = 2; + private static final String DELIMITER = " "; + private static final int VALID_REQUEST_LINE_SIZE = 3; + private static final String QUERY_STRING_BEGIN = "?"; + private static final int EMPTY_QUERY_STRING = -1; + + private final HttpMethod httpMethod; + private final String uri; + private final String httpVersion; + + private RequestLine(final HttpMethod httpMethod, final String uri, final String httpVersion) { + this.httpMethod = httpMethod; + this.uri = uri; + this.httpVersion = httpVersion; + } + + public static RequestLine from(final String line) { + final String[] requestLine = line.split(DELIMITER); + validate(requestLine); + return new RequestLine( + HttpMethod.from(requestLine[HTTP_METHOD_INDEX]), + requestLine[URI_INDEX], + requestLine[HTTP_VERSION_INDEX] + ); + } + + private static void validate(final String[] requestLine) { + if (requestLine.length != VALID_REQUEST_LINE_SIZE) { + throw new InvalidRequestLineException(); + } + } + + public String parseUriWithOutQueryString() { + final int queryStringIndex = uri.indexOf(QUERY_STRING_BEGIN); + if (queryStringIndex == EMPTY_QUERY_STRING) { + return uri; + } + return uri.substring(0, queryStringIndex); + } + + public QueryString parseQueryString() { + return QueryString.from(uri); + } + + public HttpMethod getHttpMethod() { + return httpMethod; + } + + public String getUri() { + return uri; + } + + public String getHttpVersion() { + return httpVersion; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseGenerator.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseGenerator.java new file mode 100644 index 0000000000..8423da2bbe --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseGenerator.java @@ -0,0 +1,76 @@ +package org.apache.coyote.http11.response; + +import static org.apache.coyote.http11.common.Constants.BLANK; +import static org.apache.coyote.http11.common.Constants.CRLF; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import org.apache.coyote.http11.common.HttpExtensionType; +import org.apache.coyote.http11.common.HttpStatus; + +public class HttpResponseGenerator { + + private final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + + public String generate(final ResponseEntity responseEntity) throws IOException { + final String uri = responseEntity.getUri(); + if (uri.equals("/")) { + return generateResponse(responseEntity, "Hello world!"); + } + if (responseEntity.getHttpStatus() == HttpStatus.FOUND) { + return generateRedirectResponse(responseEntity); + } + final String responseBody = readStaticFile(uri); + return generateResponse(responseEntity, responseBody); + } + + private String generateResponse(final ResponseEntity responseEntity, final String responseBody) { + return String.join( + CRLF, + generateHttpStatusLine(responseEntity.getHttpStatus()), + generateContentTypeLine(responseEntity.getHttpExtensionType()), + generateContentLengthLine(responseBody), + "", + responseBody + ); + } + + private String generateHttpStatusLine(final HttpStatus httpStatus) { + return String.join(BLANK, "HTTP/1.1", httpStatus.getCode(), httpStatus.name(), ""); + } + + private String generateContentTypeLine(final HttpExtensionType httpExtensionType) { + return "Content-Type: " + httpExtensionType.getContentType() + ";charset=utf-8 "; + } + + private CharSequence generateContentLengthLine(final String responseBody) { + return "Content-Length: " + responseBody.getBytes().length + BLANK; + } + + private String readStaticFile(final String uri) throws IOException { + final URL resource = classLoader.getResource("static" + uri); + final File file = new File(resource.getFile()); + return new String(Files.readAllBytes(file.toPath())); + } + + private String generateRedirectResponse(final ResponseEntity responseEntity) { + final HttpStatus httpStatus = responseEntity.getHttpStatus(); + final String firstLine = String.join(BLANK, "HTTP/1.1", httpStatus.getCode(), httpStatus.name(), ""); + return String.join( + CRLF, + firstLine, + "Location: " + responseEntity.getUri(), + generateSetCookieLine(responseEntity) + ); + } + + private String generateSetCookieLine(final ResponseEntity responseEntity) { + final String jsessionid = responseEntity.getHttpCookie().get("JSESSIONID"); + if (jsessionid == null) { + return ""; + } + return "Set-Cookie: JSESSIONID=" + jsessionid + " "; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseEntity.java b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseEntity.java new file mode 100644 index 0000000000..0f5db6628b --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseEntity.java @@ -0,0 +1,48 @@ +package org.apache.coyote.http11.response; + +import org.apache.coyote.http11.common.HttpCookie; +import org.apache.coyote.http11.common.HttpExtensionType; +import org.apache.coyote.http11.common.HttpStatus; + +public class ResponseEntity { + + private static final String JSESSION_ID = "JSESSIONID"; + + private final HttpStatus httpStatus; + private final String uri; + private final HttpCookie httpCookie; + + public ResponseEntity(final HttpStatus httpStatus, final String uri) { + this(httpStatus, uri, new HttpCookie()); + } + + public ResponseEntity(final HttpStatus httpStatus, final String uri, final HttpCookie httpCookie) { + this.httpStatus = httpStatus; + this.uri = uri; + this.httpCookie = httpCookie; + } + + public void setCookie(final String key, final String value) { + httpCookie.put(key, value); + } + + public void setJSessionId(final String id) { + httpCookie.put(JSESSION_ID, id); + } + + public HttpExtensionType getHttpExtensionType() { + return HttpExtensionType.from(uri); + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + + public String getUri() { + return uri; + } + + public HttpCookie getHttpCookie() { + return httpCookie; + } +} diff --git a/tomcat/src/main/resources/static/409.html b/tomcat/src/main/resources/static/409.html new file mode 100644 index 0000000000..658b2c1d27 --- /dev/null +++ b/tomcat/src/main/resources/static/409.html @@ -0,0 +1,52 @@ + + + + + + + + + 409 Conflict - SB Admin + + + + +

+
+
+
+
+
+
+

account conflict.

+ + + Return to Dashboard + +
+
+
+
+
+
+ +
+ + + + diff --git a/tomcat/src/main/resources/static/favicon.ico b/tomcat/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..1bf3bff134 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -1,66 +1,70 @@ - - - - - - - 로그인 - - - - -
-
-
-
-
-
-
-

로그인

-
-
-
- - -
-
- - -
-
- -
-
+ + + + + + + 로그인 + + + + +
+
+
+
+
+
+
+

로그인

+
+
+
+ +
- +
+ +
+
+
+
-
+
-
+
+
- - - - + + + + + + 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 deleted file mode 100644 index 512b919f09..0000000000 --- a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package nextstep.org.apache.coyote.http11; - -import support.StubSocket; -import org.apache.coyote.http11.Http11Processor; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; - -import static org.assertj.core.api.Assertions.assertThat; - -class Http11ProcessorTest { - - @Test - void process() { - // given - final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // then - var expected = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: 12 ", - "", - "Hello world!"); - - assertThat(socket.output()).isEqualTo(expected); - } - - @Test - void index() throws IOException { - // given - final String httpRequest= String.join("\r\n", - "GET /index.html HTTP/1.1 ", - "Host: localhost:8080 ", - "Connection: keep-alive ", - "", - ""); - - final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // 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" + - "\r\n"+ - new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - - assertThat(socket.output()).isEqualTo(expected); - } -} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java new file mode 100644 index 0000000000..d221405d2d --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -0,0 +1,229 @@ +package org.apache.coyote.http11; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +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 support.StubSocket; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class Http11ProcessorTest { + + @Test + void process() { + // given + final var socket = new StubSocket(); + final var processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String expected = String.join("\r\n", + "HTTP/1.1 200 OK ", + "Content-Type: text/html;charset=utf-8 ", + "Content-Length: 12 ", + "", + "Hello world!"); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + void index() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /index.html HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/index.html"); + final String 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())); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Nested + class 로그인 { + + @Test + void 페이지_접속시_200_OK_를_반환한다() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/login.html"); + final String responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + final String expected = "HTTP/1.1 200 OK \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Content-Length: " + responseBody.getBytes().length + " \r\n" + + "\r\n" + + responseBody; + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + void 성공_시_302_FOUND를_반환한다() { + // given + final String content = "account=gugu&password=password"; + final String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Content-Length: " + content.getBytes().length, + "", + content); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String expected = "HTTP/1.1 302 FOUND \r\nLocation: /index.html"; + assertThat(socket.output()).contains(expected); + } + + @Test + void 실패_시_401_UNAUTHORIZE를_반환한다() throws IOException { + // given + final String content = "account=gugu&password=password2"; + final String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Content-Length: " + content.getBytes().length, + "", + content); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/401.html"); + final String responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + final String expected = "HTTP/1.1 401 UNAUTHORIZED \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Content-Length: " + responseBody.getBytes().length + " \r\n" + + "\r\n" + + responseBody; + assertThat(socket.output()).isEqualTo(expected); + } + } + + @Nested + class 회원가입 { + + @Test + void 페이지_접속시_200_OK_를_반환한다() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/register.html"); + final String responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + final String expected = "HTTP/1.1 200 OK \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Content-Length: " + responseBody.getBytes().length + " \r\n" + + "\r\n" + + responseBody; + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + void 성공_시_302_FOUND를_반환한다() { + // given + final String content = "account=gugu2&password=password&email=hkkang@woowahan.com"; + final String httpRequest = String.join("\r\n", + "POST /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Content-Length: " + content.getBytes().length, + "", + content); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String expected = "HTTP/1.1 302 FOUND \r\nLocation: /index.html"; + assertThat(socket.output()).contains(expected); + } + + @Test + void 닉네임_중복시_409_CONFLICT를_반환한다() throws IOException { + // given + final String content = "account=gugu&password=password&email=hkkang@woowahan.com"; + final String httpRequest = String.join("\r\n", + "POST /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Content-Length: " + content.getBytes().length, + "", + content); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/409.html"); + final String responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + final String expected = "HTTP/1.1 409 CONFLICT \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Content-Length: " + responseBody.getBytes().length + " \r\n" + + "\r\n" + + responseBody; + assertThat(socket.output()).isEqualTo(expected); + } + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/ContentTypeTest.java b/tomcat/src/test/java/org/apache/coyote/http11/common/ContentTypeTest.java new file mode 100644 index 0000000000..1e7b88f771 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/common/ContentTypeTest.java @@ -0,0 +1,36 @@ +package org.apache.coyote.http11.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ContentTypeTest { + + @Test + void 파일명을_입력받아_해당_파일에_해당하는_컨텐츠_타입을_반환한다() { + // given + final String fileName = "hello.js"; + + // when + final HttpExtensionType httpExtensionType = HttpExtensionType.from(fileName); + + // then + assertThat(httpExtensionType).isEqualTo(HttpExtensionType.JS); + } + + @Test + void 입력받은_파일에_해당하는_컨텐츠_타입이_없는_경우_HTML타입을_반환한다() { + // given + final String fileName = "hello.txt"; + + // when + final HttpExtensionType httpExtensionType = HttpExtensionType.from(fileName); + + // then + assertThat(httpExtensionType).isEqualTo(HttpExtensionType.HTML); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/HttpCookieTest.java b/tomcat/src/test/java/org/apache/coyote/http11/common/HttpCookieTest.java new file mode 100644 index 0000000000..e0b24f04b6 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/common/HttpCookieTest.java @@ -0,0 +1,63 @@ +package org.apache.coyote.http11.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.util.UUID; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpCookieTest { + + @Test + void cookie_문자열을_입력받아_HttpCookie를_반환한다() { + // given + final String cookie = "yummy_cookie=choco; JSESSIONID=656cef62-e3c4-40bc-a8df-94732920ed46"; + + // when + final HttpCookie httpCookie = HttpCookie.from(cookie); + + // then + assertThat(httpCookie.getItems()).contains( + entry("yummy_cookie", "choco"), + entry("JSESSIONID", "656cef62-e3c4-40bc-a8df-94732920ed46") + ); + } + + @Test + void key와_value를_입력받아_저장한다() { + // given + final HttpCookie httpCookie = new HttpCookie(); + + // when + httpCookie.put("hello", "world"); + + // then + assertThat(httpCookie.get("hello")).isEqualTo("world"); + } + + @Test + void key를_입력받아_값을_반환한다() { + // given + final String uuid = UUID.randomUUID().toString(); + final String body = "JSESSIONID=" + uuid; + final HttpCookie cookie = HttpCookie.from(body); + + // expect + assertThat(cookie.get("JSESSIONID")).isEqualTo(uuid); + } + + @Test + void jSessionId를_가져온다() { + // given + final String uuid = UUID.randomUUID().toString(); + final String body = "JSESSIONID=" + uuid; + final HttpCookie cookie = HttpCookie.from(body); + + // expect + assertThat(cookie.getJSessionId()).isEqualTo(uuid); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/HttpMethodTest.java b/tomcat/src/test/java/org/apache/coyote/http11/common/HttpMethodTest.java new file mode 100644 index 0000000000..eeb80d192f --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/common/HttpMethodTest.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.common; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.coyote.http11.exception.InvalidHttpMethodException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpMethodTest { + + @Test + void 올바르지_않은_HttpMethod인_경우_InvalidHttpMethodException_예외를_던진다() { + // given + final String http = "GET2"; + + // expect + assertThatThrownBy(() -> HttpMethod.from(http)) + .isInstanceOf(InvalidHttpMethodException.class) + .hasMessage("올바르지 않은 HttpMethod 형식입니다."); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/SessionManagerTest.java b/tomcat/src/test/java/org/apache/coyote/http11/common/SessionManagerTest.java new file mode 100644 index 0000000000..23c59319eb --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/common/SessionManagerTest.java @@ -0,0 +1,52 @@ +package org.apache.coyote.http11.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class SessionManagerTest { + + private final SessionManager sessionManager = new SessionManager(); + + @Test + void 세션을_추가한다() { + // given + final Session session = new Session("hello"); + + // when + sessionManager.add(session); + + // then + assertThat(sessionManager.findSession("hello")).isEqualTo(session); + } + + @Test + void id를_입력받아_세션을_반환한다() { + // given + final Session session = new Session("world"); + sessionManager.add(session); + + // when + final Session findSession = sessionManager.findSession("world"); + + // then + assertThat(findSession).isEqualTo(session); + } + + @Test + void 세션을_제거한다() { + // given + final Session session = new Session("helloworld"); + sessionManager.add(session); + + // when + sessionManager.remove("helloworld"); + + // then + assertThat(sessionManager.findSession("helloworld")).isNull(); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/SessionTest.java b/tomcat/src/test/java/org/apache/coyote/http11/common/SessionTest.java new file mode 100644 index 0000000000..af13cbef4d --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/common/SessionTest.java @@ -0,0 +1,61 @@ +package org.apache.coyote.http11.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class SessionTest { + + @Test + void 값을_설정한다() { + // given + final Session session = new Session("UUID"); + + // when + session.setAttribute("hello", "world"); + + // then + assertThat(session.getAttribute("hello")).isEqualTo("world"); + } + + @Test + void 값을_가져온다() { + // given + final Session session = new Session("UUID"); + session.setAttribute("hello", "world"); + + // expect + assertThat(session.getAttribute("hello")).isEqualTo("world"); + } + + @Test + void 값을_제거한다() { + // given + final Session session = new Session("UUID"); + session.setAttribute("hello", "world"); + + // when + session.removeAttribute("hello"); + + // then + assertThat(session.getAttribute("hello")).isNull(); + } + + @Test + void 세션에_있는_모든_값을_제거한다() { + // given + final Session session = new Session("UUID"); + session.setAttribute("hello", "world"); + session.setAttribute("hello2", "world2"); + + // when + session.invalidate(); + + // then + assertThat(session.getItems()).isEmpty(); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/QueryStringTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/QueryStringTest.java new file mode 100644 index 0000000000..d2f2aef09a --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/QueryStringTest.java @@ -0,0 +1,28 @@ +package org.apache.coyote.http11.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class QueryStringTest { + + @Test + void uri를_입력받아_QueryString을_생성한다() { + // given + final String uri = "/login?account=gugu&password=password"; + + // when + final QueryString queryString = QueryString.from(uri); + + // then + assertThat(queryString.getItems()).contains( + entry("account", "gugu"), + entry("password", "password") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestBodyTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestBodyTest.java new file mode 100644 index 0000000000..bc6ae51778 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestBodyTest.java @@ -0,0 +1,46 @@ +package org.apache.coyote.http11.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RequestBodyTest { + + @Test + void body_문자열을_입력받아_RequestBody를_반환한다() { + // given + final String body = "account=gugu&password=password&email=hkkang@woowahan.com"; + + // when + final RequestBody requestBody = RequestBody.from(body); + + // then + assertThat(requestBody.getItems()).contains( + entry("account", "gugu"), + entry("password", "password"), + entry("email", "hkkang@woowahan.com") + ); + } + + @Test + void 빈_RequestBody를_반환한다() { + // expect + final RequestBody requestBody = RequestBody.empty(); + assertThat(requestBody.getItems()).isEmpty(); + } + + @Test + void key를_입력받아_값을_반환한다() { + // given + final String body = "account=gugu&password=password&email=hkkang@woowahan.com"; + final RequestBody requestBody = RequestBody.from(body); + + // expect + assertThat(requestBody.get("account")).isEqualTo("gugu"); + } +} 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..506d4bb724 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestHeaderTest.java @@ -0,0 +1,46 @@ +package org.apache.coyote.http11.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import org.apache.coyote.http11.common.HttpCookie; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RequestHeaderTest { + + @Test + void header_문자열을_입력받아_RequestHeader를_반환한다() { + // given + final String header = "Connection: keep-alive\r\nSec-Fetch-Dest: image\r\n"; + + // when + final RequestHeader requestHeader = RequestHeader.from(header); + + // then + assertThat(requestHeader.getItems()).contains( + entry("Connection", "keep-alive"), + entry("Sec-Fetch-Dest", "image") + ); + } + + @Test + void 쿠키값을_파싱하여_반환한다() { + // given + final String header = "Cookie: yummy_cookie=choco; JSESSIONID=656cef62-e3c4-40bc-a8df-94732920ed46\r\n" + + "Sec-Fetch-Dest: image\r\n"; + final RequestHeader requestHeader = RequestHeader.from(header); + + // when + final HttpCookie httpCookie = requestHeader.parseCookie(); + + // then + assertThat(httpCookie.getItems()).contains( + entry("yummy_cookie", "choco"), + entry("JSESSIONID", "656cef62-e3c4-40bc-a8df-94732920ed46") + ); + } +} 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..0798199e60 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestLineTest.java @@ -0,0 +1,73 @@ +package org.apache.coyote.http11.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.apache.coyote.http11.common.HttpMethod; +import org.apache.coyote.http11.exception.InvalidRequestLineException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RequestLineTest { + + @Test + void 올바르지_않은_RequestLine_형식을_입력받는_경우_InvalidRequestLineException_예외를_던진다() { + // given + final String line = "GET /index.html HTTP/1.1 hello"; + + // expect + assertThatThrownBy(() -> RequestLine.from(line)) + .isInstanceOf(InvalidRequestLineException.class) + .hasMessage("올바르지 않은 RequestLine 형식입니다."); + } + + @Test + void 올바른_RequestLine_형식인_경우_RequestLine_객체가_정상적으로_생성된다() { + // given + final String line = "GET /index.html HTTP/1.1"; + + // when + final RequestLine requestLine = RequestLine.from(line); + + // then + assertAll( + () -> assertThat(requestLine.getHttpMethod()).isEqualTo(HttpMethod.GET), + () -> assertThat(requestLine.getUri()).isEqualTo("/index.html"), + () -> assertThat(requestLine.getHttpVersion()).isEqualTo("HTTP/1.1") + ); + } + + @Test + void queryString을_제외한_uri을_반환한다() { + // given + final String line = "GET /login?account=gugu&password=password HTTP/1.1"; + final RequestLine requestLine = RequestLine.from(line); + + // when + final String result = requestLine.parseUriWithOutQueryString(); + + // then + assertThat(result).isEqualTo("/login"); + } + + @Test + void queryString을_파싱하여_반환한다() { + // given + final String line = "GET /login?account=gugu&password=password HTTP/1.1"; + final RequestLine requestLine = RequestLine.from(line); + + // when + final QueryString queryString = requestLine.parseQueryString(); + + // then + assertThat(queryString.getItems()).contains( + entry("account", "gugu"), + entry("password", "password") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseGeneratorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseGeneratorTest.java new file mode 100644 index 0000000000..409e67bea8 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseGeneratorTest.java @@ -0,0 +1,78 @@ +package org.apache.coyote.http11.response; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import org.apache.coyote.http11.common.HttpStatus; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpResponseGeneratorTest { + + private final HttpResponseGenerator httpResponseGenerator = new HttpResponseGenerator(); + + @Test + void 입력받은_ResponseEntity의_uri가_루트인_경우_Hello_world가_담긴_HttpResponse를_반환한다() throws IOException { + // given + final ResponseEntity responseEntity = new ResponseEntity(HttpStatus.OK, "/"); + + // when + final String actual = httpResponseGenerator.generate(responseEntity); + + // then + final String expected = String.join( + "\r\n", + "HTTP/1.1 200 OK ", + "Content-Type: text/html;charset=utf-8 ", + "Content-Length: 12 ", + "", + "Hello world!" + ); + assertThat(actual).isEqualTo(expected); + } + + @Test + void 입력받은_HttpStatus에_따라_HttpResponse의_상태가_달라진다() throws IOException { + // given + final ResponseEntity responseEntity = new ResponseEntity(HttpStatus.UNAUTHORIZED, "/"); + + // when + final String actual = httpResponseGenerator.generate(responseEntity); + + // then + final String expected = String.join( + "\r\n", + "HTTP/1.1 401 UNAUTHORIZED ", + "Content-Type: text/html;charset=utf-8 ", + "Content-Length: 12 ", + "", + "Hello world!" + ); + assertThat(actual).isEqualTo(expected); + } + + @Test + void 입력받은_uri에_해당하는_파일을_읽어와_HttpResponse를_반환한다() throws IOException { + // given + final ResponseEntity responseEntity = new ResponseEntity(HttpStatus.OK, "/index.html"); + + // when + final String actual = httpResponseGenerator.generate(responseEntity); + + // then + final URL resource = getClass().getClassLoader().getResource("static/index.html"); + final String 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())); + + assertThat(actual).isEqualTo(expected); + } +}