diff --git a/study/build.gradle b/study/build.gradle index 87a1f0313c..7bdce4f751 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -19,6 +19,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.apache.commons:commons-lang3:3.14.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.4.1' diff --git a/study/src/main/java/cache/com/example/GreetingController.java b/study/src/main/java/cache/com/example/GreetingController.java index c0053cda42..2f29d87468 100644 --- a/study/src/main/java/cache/com/example/GreetingController.java +++ b/study/src/main/java/cache/com/example/GreetingController.java @@ -1,15 +1,21 @@ package cache.com.example; +import cache.com.example.version.ResourceVersion; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import jakarta.servlet.http.HttpServletResponse; - @Controller public class GreetingController { + private final ResourceVersion version; + + public GreetingController(ResourceVersion version) { + this.version = version; + } + @GetMapping("/") public String index() { return "index"; diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheHandlerInterceptor.java b/study/src/main/java/cache/com/example/cachecontrol/CacheHandlerInterceptor.java new file mode 100644 index 0000000000..ed47d0541a --- /dev/null +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheHandlerInterceptor.java @@ -0,0 +1,18 @@ +package cache.com.example.cachecontrol; + +import com.google.common.net.HttpHeaders; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.CacheControl; +import org.springframework.web.servlet.HandlerInterceptor; + +public class CacheHandlerInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + CacheControl cacheControl = CacheControl.noCache().cachePrivate(); + response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControl.getHeaderValue()); + + return true; + } +} 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..f3e04d3a3d 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,6 +1,12 @@ package cache.com.example.cachecontrol; +import com.google.common.net.HttpHeaders; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.time.Duration; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -9,5 +15,14 @@ public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + registry.addInterceptor(new CacheHandlerInterceptor()).addPathPatterns("/"); + registry.addInterceptor(new HandlerInterceptor() { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + CacheControl cacheControl = CacheControl.maxAge(Duration.ofDays(365)).cachePublic(); + response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControl.getHeaderValue()); + return true; + } + }).addPathPatterns("/resources/**"); } } 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..c51500216c 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,15 @@ 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; @Configuration public class EtagFilterConfiguration { -// @Bean -// public FilterRegistrationBean shallowEtagHeaderFilter() { -// return null; -// } + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + return new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index e3503a5fb9..8b74bdfd88 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -8,3 +8,6 @@ server: threads: min-spare: 2 max: 2 + compression: + enabled: true + min-response-size: 10 diff --git a/study/src/test/java/cache/com/example/GreetingControllerTest.java b/study/src/test/java/cache/com/example/GreetingControllerTest.java index 9ce2a394f7..2fea3e6dfb 100644 --- a/study/src/test/java/cache/com/example/GreetingControllerTest.java +++ b/study/src/test/java/cache/com/example/GreetingControllerTest.java @@ -1,6 +1,9 @@ package cache.com.example; +import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES; + import cache.com.example.version.ResourceVersion; +import java.time.Duration; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,10 +13,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.test.web.reactive.server.WebTestClient; -import java.time.Duration; - -import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class GreetingControllerTest { @@ -68,10 +67,8 @@ void testETag() { } /** - * http://localhost:8080/resource-versioning - * 위 url의 html 파일에서 사용하는 js, css와 같은 정적 파일에 캐싱을 적용한다. - * 보통 정적 파일을 캐싱 무효화하기 위해 캐싱과 함께 버전을 적용시킨다. - * 정적 파일에 변경 사항이 생기면 배포할 때 버전을 바꿔주면 적용된 캐싱을 무효화(Caching Busting)할 수 있다. + * http://localhost:8080/resource-versioning 위 url의 html 파일에서 사용하는 js, css와 같은 정적 파일에 캐싱을 적용한다. 보통 정적 파일을 캐싱 무효화하기 + * 위해 캐싱과 함께 버전을 적용시킨다. 정적 파일에 변경 사항이 생기면 배포할 때 버전을 바꿔주면 적용된 캐싱을 무효화(Caching Busting)할 수 있다. */ @Test void testCacheBustingOfStaticResources() { diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..642f00cdbd 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,53 +1,56 @@ package study; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; +import java.nio.file.Paths; import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; /** - * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. - * File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다. + * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다. */ @DisplayName("File 클래스 학습 테스트") class FileTest { /** * resource 디렉터리 경로 찾기 - * - * File 객체를 생성하려면 파일의 경로를 알아야 한다. - * 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. - * resource 디렉터리의 경로는 어떻게 알아낼 수 있을까? + *

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

+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws URISyntaxException, IOException { final String fileName = "nextstep.txt"; // todo - final Path path = null; + URL url = getClass().getClassLoader().getResource(fileName); + assertThat(url).isNotNull(); + final Path path = Paths.get(url.toURI()); // todo - final List actual = Collections.emptyList(); + final List actual = Files.readAllLines(path); assertThat(actual).containsOnly("nextstep"); } diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..aa98d51bfa 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,45 +1,50 @@ 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.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)을 + * 처리할 수 있다. */ @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바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다. @@ -50,10 +55,10 @@ class OutputStream_학습_테스트 { final OutputStream outputStream = new ByteArrayOutputStream(bytes.length); /** - * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ + outputStream.write(bytes); final String actual = outputStream.toString(); assertThat(actual).isEqualTo("nextstep"); @@ -61,62 +66,57 @@ class OutputStream_학습_테스트 { } /** - * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. - * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. - * - * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. - * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. - * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 - * 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다. + * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. + *

+ * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. Stream은 + * 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다. */ @Test void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException { final OutputStream outputStream = mock(BufferedOutputStream.class); /** - * todo * 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(); } } /** * 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 @@ -125,10 +125,15 @@ class InputStream_학습_테스트 { final InputStream inputStream = new ByteArrayInputStream(bytes); /** - * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + + byte[] data = new byte[bytes.length]; + int buf; + for (int i = 0; i < bytes.length && (buf = inputStream.read()) != -1; i++) { + data[i] = (byte) buf; + } + final String actual = new String(data); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -136,8 +141,7 @@ class InputStream_학습_테스트 { } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException { @@ -149,32 +153,32 @@ class InputStream_학습_테스트 { * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (inputStream) { + } verify(inputStream, atLeastOnce()).close(); } } /** * 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 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. 버퍼 크기를 지정하지 + * 않으면 버퍼의 기본 사이즈는 얼마일까? */ @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 +186,30 @@ 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()); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); final StringBuilder actual = new StringBuilder(); + while (bufferedReader.ready()) { + actual.append(bufferedReader.readLine()).append(System.lineSeparator()); + } assertThat(actual).hasToString(emoji); } diff --git a/tomcat/src/main/java/com/techcourse/StaticResourceReader.java b/tomcat/src/main/java/com/techcourse/StaticResourceReader.java new file mode 100644 index 0000000000..aed229a387 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/StaticResourceReader.java @@ -0,0 +1,23 @@ +package com.techcourse; + + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Paths; + +public class StaticResourceReader { + + private final static String BASE_PATH = "static"; + + private final ClassLoader classLoader = getClass().getClassLoader(); + + public byte[] read(String path) throws IOException { + try (InputStream resourceAsStream = classLoader.getResourceAsStream(Paths.get(BASE_PATH, path).toString())) { + if (resourceAsStream == null) { + return null; + } + + return resourceAsStream.readAllBytes(); + } + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/AbstractController.java b/tomcat/src/main/java/com/techcourse/controller/AbstractController.java new file mode 100644 index 0000000000..cada57aae4 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/AbstractController.java @@ -0,0 +1,21 @@ +package com.techcourse.controller; + +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; + +public abstract class AbstractController implements Controller { + + @Override + public void service(HttpRequest request, HttpResponse.Builder responseBuilder) { + if (request.method().equals("GET")) { + doGet(request, responseBuilder); + } + if (request.method().equals("POST")) { + doPost(request, responseBuilder); + } + } + + protected void doPost(HttpRequest request, HttpResponse.Builder responseBuilder) { /* NOOP */ } + + protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { /* NOOP */ } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/Controller.java b/tomcat/src/main/java/com/techcourse/controller/Controller.java new file mode 100644 index 0000000000..ef38bd1ec4 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/Controller.java @@ -0,0 +1,8 @@ +package com.techcourse.controller; + +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; + +public interface Controller { + void service(HttpRequest request, HttpResponse.Builder responseBuilder); +} diff --git a/tomcat/src/main/java/com/techcourse/controller/HomeController.java b/tomcat/src/main/java/com/techcourse/controller/HomeController.java new file mode 100644 index 0000000000..47141dcc32 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/HomeController.java @@ -0,0 +1,15 @@ +package com.techcourse.controller; + +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.Status; + +public class HomeController extends AbstractController { + + @Override + protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { + responseBuilder.status(Status.OK) + .contentType("text/html") + .body("Hello world!".getBytes()); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/LoginController.java b/tomcat/src/main/java/com/techcourse/controller/LoginController.java new file mode 100644 index 0000000000..2b8f24a9fb --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/LoginController.java @@ -0,0 +1,74 @@ +package com.techcourse.controller; + +import static org.reflections.Reflections.log; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import jakarta.servlet.http.HttpSession; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import org.apache.catalina.session.JSession; +import org.apache.catalina.session.SessionManager; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.HttpResponse.Builder; +import org.apache.coyote.http11.Status; + +public class LoginController extends AbstractController { + + private final ResourceController resourceController; + + public LoginController() { + this.resourceController = new ResourceController(); + } + + private static void processSessionLogin(Builder responseBuilder, HttpSession session) { + User user = (User) Objects.requireNonNull(session).getAttribute("user"); + + log.info("이미 로그인한 사용자 입니다. - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); + + responseBuilder.status(Status.FOUND) + .location("/index.html"); + } + + private static void processAccountLogin(Builder responseBuilder, User user) { + String sessionId = UUID.randomUUID().toString(); + JSession jSession = new JSession(sessionId); + jSession.setAttribute("user", user); + SessionManager.getInstance().add(jSession); + + log.info("계정 정보 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), sessionId); + + responseBuilder.status(Status.FOUND) + .location("/index.html") + .addCookie(JSession.COOKIE_NAME, sessionId); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { + HttpSession session = SessionManager.getInstance().getSession(request); + if (session != null) { + processSessionLogin(responseBuilder, session); + return; + } + + if (request.parameters().containsKey("account") && request.parameters().containsKey("password")) { + findValidUser(request).ifPresentOrElse( + user -> processAccountLogin(responseBuilder, user), + () -> responseBuilder.status(Status.FOUND).location("/401.html") + ); + return; + } + + resourceController.doGet(request.updatePath("login.html"), responseBuilder); + } + + private Optional findValidUser(HttpRequest request) { + String account = request.parameters().get("account"); + String password = request.parameters().get("password"); + + return InMemoryUserRepository.findByAccount(account) + .filter(user -> user.checkPassword(password)); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/RegisterController.java b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java new file mode 100644 index 0000000000..5ba88730f2 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java @@ -0,0 +1,44 @@ +package com.techcourse.controller; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import java.util.Map; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.Status; + +public class RegisterController extends AbstractController { + + private final ResourceController resourceController; + + public RegisterController() { + this.resourceController = new ResourceController(); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { + resourceController.doGet(request.updatePath("register.html"), responseBuilder); + } + + @Override + protected void doPost(HttpRequest request, HttpResponse.Builder responseBuilder) { + Map body = HttpRequest.extractParameters(request.body()); + + if (!body.containsKey("account") || + !body.containsKey("password") || + !body.containsKey("email")) { + responseBuilder.status(Status.BAD_REQUEST); + return; + } + + String account = body.get("account"); + if (InMemoryUserRepository.findByAccount(account).isPresent()) { + responseBuilder.status(Status.CONFLICT); + return; + } + + InMemoryUserRepository.save(new User(account, body.get("password"), body.get("email"))); + responseBuilder.status(Status.FOUND) + .location("/index.html"); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java b/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java new file mode 100644 index 0000000000..a7587aac2d --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java @@ -0,0 +1,28 @@ +package com.techcourse.controller; + +import java.util.Map; +import java.util.function.Predicate; +import org.apache.coyote.http11.HttpRequest; + +public class RequestMapping { + + private final Controller defaultController; + private final Map, Controller> controllers; + + public RequestMapping() { + this.defaultController = new ResourceController(); + this.controllers = Map.of( + req -> req.path().equals("/"), new HomeController(), + req -> req.path().equals("/login"), new LoginController(), + req -> req.path().equals("/register"), new RegisterController() + ); + } + + public Controller getController(HttpRequest request) { + return controllers.entrySet().stream() + .filter((entry) -> entry.getKey().test(request)) + .findAny() + .map(Map.Entry::getValue) + .orElse(defaultController); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/ResourceController.java b/tomcat/src/main/java/com/techcourse/controller/ResourceController.java new file mode 100644 index 0000000000..fb596f3721 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/ResourceController.java @@ -0,0 +1,42 @@ +package com.techcourse.controller; + +import com.techcourse.StaticResourceReader; +import java.io.IOException; +import java.net.URLConnection; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ResourceController extends AbstractController { + + private static final Logger log = LoggerFactory.getLogger(ResourceController.class); + + private final StaticResourceReader staticResourceReader; + + public ResourceController() { + this.staticResourceReader = new StaticResourceReader(); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { + String contentType = URLConnection.guessContentTypeFromName(request.path()); + final byte[] responseBody; + try { + responseBody = staticResourceReader.read(request.path()); + if (responseBody == null) { + responseBuilder.status(Status.NOT_FOUND) + .build(); + } + responseBuilder.status(Status.OK) + .contentType(contentType) + .body(responseBody) + .build(); + } catch (IOException e) { + log.error(e.getMessage(), e); + responseBuilder.status(Status.INTERNAL_SERVER_ERROR) + .build(); + } + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/session/JSession.java b/tomcat/src/main/java/org/apache/catalina/session/JSession.java new file mode 100644 index 0000000000..cabce50005 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/JSession.java @@ -0,0 +1,102 @@ +package org.apache.catalina.session; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionContext; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +public class JSession implements HttpSession { + + public static final String COOKIE_NAME = "JSESSIONID"; + + private final String id; + private final Map attributes = new HashMap<>(); + private final long creationTime = System.currentTimeMillis(); + + public JSession(String id) { + this.id = id; + } + + @Override + public long getCreationTime() { + return creationTime; + } + + @Override + public String getId() { + return id; + } + + @Override + public long getLastAccessedTime() { + return 0; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public int getMaxInactiveInterval() { + return 0; + } + + @Override + public void setMaxInactiveInterval(int i) { + } + + @Override + public HttpSessionContext getSessionContext() { + return null; + } + + @Override + public Object getAttribute(String s) { + return attributes.get(s); + } + + @Override + public Object getValue(String s) { + return null; + } + + @Override + public Enumeration getAttributeNames() { + return null; + } + + @Override + public String[] getValueNames() { + return new String[0]; + } + + @Override + public void setAttribute(String s, Object o) { + attributes.put(s, o); + } + + @Override + public void putValue(String s, Object o) { + } + + @Override + public void removeAttribute(String s) { + attributes.remove(s); + } + + @Override + public void removeValue(String s) { + } + + @Override + public void invalidate() { + } + + @Override + public boolean isNew() { + return false; + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java new file mode 100644 index 0000000000..efa41aaeb8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -0,0 +1,50 @@ +package org.apache.catalina.session; + +import jakarta.servlet.http.HttpSession; +import java.util.HashMap; +import java.util.Map; +import org.apache.catalina.Manager; +import org.apache.coyote.http11.HttpRequest; + +public class SessionManager implements Manager { + + private static final Map SESSIONS = new HashMap<>(); + private static final SessionManager SESSION_MANAGER = new SessionManager(); + + private SessionManager() { + } + + public static SessionManager getInstance() { + return SESSION_MANAGER; + } + + @Override + public void add(HttpSession session) { + SESSIONS.put(session.getId(), session); + } + + @Override + public HttpSession findSession(String id) { + HttpSession session = SESSIONS.get(id); + + if (session == null || session.getAttribute("user") == null) { + return null; + } + + return session; + } + + @Override + public void remove(HttpSession session) { + SESSIONS.remove(session.getId()); + } + + public HttpSession getSession(HttpRequest request) { + String sessionId = request.cookies().get(JSession.COOKIE_NAME); + if (sessionId == null) { + return null; + } + + return SESSIONS.get(sessionId); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java new file mode 100644 index 0000000000..290fb164df --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java @@ -0,0 +1,70 @@ +package org.apache.coyote.http11; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Http11InputStreamReader { + + private static final Logger log = LoggerFactory.getLogger(Http11InputStreamReader.class); + + public static List read(InputStream inputStream) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + + List lines = new ArrayList<>(readHeaders(reader)); + int contentLength = getContentLength(lines); + lines.add(readBody(contentLength, reader)); + + return lines; + } + + private static int getContentLength(List headers) { + for (String headerLine : headers) { + if (headerLine.toLowerCase().startsWith("content-length:")) { + try { + return Integer.parseInt(headerLine.split(":")[1].trim()); + } catch (NumberFormatException e) { + log.warn("Invalid Content-Length header format"); + } + } + } + return 0; + } + + private static List readHeaders(BufferedReader reader) throws IOException { + List lines = new ArrayList<>(); + String line; + + while ((line = reader.readLine()) != null) { + lines.add(line); + if (line.isEmpty()) { + break; + } + } + if (line == null) { + log.warn("Incomplete headers received"); + } + + return lines; + } + + private static String readBody(int contentLength, BufferedReader reader) throws IOException { + StringBuilder bodyBuilder = new StringBuilder(); + + if (contentLength > 0) { + char[] body = new char[contentLength]; + int read = reader.read(body, 0, contentLength); + if (read != contentLength) { + log.warn("Not all body data was read"); + } + bodyBuilder.append(body, 0, read); + } + + return bodyBuilder.toString(); + } +} 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 bb14184757..874a906045 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,16 +1,18 @@ package org.apache.coyote.http11; +import com.techcourse.controller.RequestMapping; import com.techcourse.exception.UncheckedServletException; +import java.io.IOException; +import java.net.Socket; +import java.util.List; import org.apache.coyote.Processor; 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 RequestMapping REQUEST_MAPPING = new RequestMapping(); private final Socket connection; @@ -20,7 +22,7 @@ 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); } @@ -28,17 +30,17 @@ public void run() { public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { + List requestLines = Http11InputStreamReader.read(inputStream); + HttpRequest request = HttpRequest.parse(requestLines); +// log.debug(request.toString()); - final var responseBody = "Hello world!"; - - 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); + HttpResponse.Builder responseBuilder = HttpResponse.builder(); + REQUEST_MAPPING.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); +// log.debug(response.toString()); - outputStream.write(response.getBytes()); + outputStream.write(response.toMessage()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java new file mode 100644 index 0000000000..6a06e4c756 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java @@ -0,0 +1,104 @@ +package org.apache.coyote.http11; + +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public record HttpRequest( + String method, + String path, + Map parameters, + Map headers, + Map cookies, + String protocolVersion, + String body) { + + public static HttpRequest parse(List lines) { + String[] startLineParts = lines.getFirst().split(" "); + String method = startLineParts[0]; + + String path = ""; + Map parameters = Map.of(); + Pattern pattern = Pattern.compile("([^?]+)(\\?(.*))?"); + Matcher matcher = pattern.matcher(startLineParts[1]); + if (matcher.find()) { + path = matcher.group(1); + parameters = extractParameters(matcher.group(3)); + } + + String protocolVersion = startLineParts[2]; + Map headers = extractHeaders(lines); + Map cookies = extractCookies(headers.get("Cookie")); + + String body = extractBody(lines); + + return new HttpRequest(method, path, parameters, headers, cookies, protocolVersion, body); + } + + private static String extractBody(List lines) { + if (lines.size() > 1 && lines.get(lines.size() - 2).isEmpty()) { + return lines.getLast(); + } + return null; + } + + private static Map extractHeaders(List lines) { + Map headers = new HashMap<>(); + + for (int i = 1; i < lines.size() - 2; i++) { + String[] lineParts = lines.get(i).trim().split(": "); + if (lineParts.length >= 2) { + headers.put(lineParts[0], lineParts[1]); + } + } + + return headers; + } + + private static Map extractCookies(String cookieMessage) { + if (cookieMessage == null) { + return Map.of(); + } + + Map cookies = new HashMap<>(); + + for (String entry : cookieMessage.split("; ")) { + int delimiterIndex = entry.indexOf("="); + if (delimiterIndex == -1) { + continue; + } + + String key = entry.substring(0, delimiterIndex).trim(); + String value = entry.substring(delimiterIndex + 1).trim(); + cookies.put(key, value); + } + + return cookies; + } + + public static Map extractParameters(String query) { + Map parameters = new HashMap<>(); + + if (query != null) { + String[] pairs = query.split("&"); + for (String pair : pairs) { + String[] keyValue = pair.split("="); + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = URLDecoder.decode(keyValue[1], Charset.defaultCharset()); + parameters.put(key, value); + } + } + } + + return parameters; + } + + public HttpRequest updatePath(String path) { + return new HttpRequest(method, path, parameters, headers, cookies, protocolVersion, body); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java new file mode 100644 index 0000000000..24f32edce7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java @@ -0,0 +1,113 @@ +package org.apache.coyote.http11; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +public record HttpResponse(String protocolVersion, int statusCode, String statusText, + Map headers, Map cookies, byte[] body) { + + private static final String CRLF = "\r\n"; + + public static Builder builder() { + return new Builder().protocolVersion("HTTP/1.1"); + } + + private static byte[] mergeByteArrays(byte[] array1, byte[] array2) { + byte[] mergedArray = new byte[array1.length + array2.length]; + + System.arraycopy(array1, 0, mergedArray, 0, array1.length); + System.arraycopy(array2, 0, mergedArray, array1.length, array2.length); + + return mergedArray; + } + + public byte[] toMessage() { + StringBuilder builder = new StringBuilder(); + builder.append(protocolVersion).append(" ").append(statusCode).append(" ").append(statusText).append(" "); + headers.forEach((key, value) -> builder.append(CRLF).append(key).append(": ").append(value).append(" ")); + + if (!cookies.isEmpty()) { + String cookiesMessage = cookies.entrySet().stream() + .map(Entry::toString) + .collect(Collectors.joining("; ")); + builder.append(CRLF).append("Set-Cookie: ").append(cookiesMessage).append(" "); + } + + if (body != null && body.length > 0) { + builder.append(CRLF).append("Content-Length: ").append(body.length).append(" "); + builder.append(CRLF.repeat(2)); + return mergeByteArrays(builder.toString().getBytes(), body); + } + + return builder.toString().getBytes(); + } + + public static class Builder { + private final Map headers; + private final Map cookies; + private String protocolVersion; + private int statusCode; + private String statusText; + private byte[] body; + + private Builder() { + headers = new HashMap<>(); + cookies = new HashMap<>(); + } + + public Builder protocolVersion(String protocolVersion) { + this.protocolVersion = protocolVersion; + return this; + } + + public Builder status(Status status) { + this.statusCode = status.getCode(); + this.statusText = status.getMessage(); + return this; + } + + public Builder addHeader(String key, String value) { + headers.put(key, value); + return this; + } + + public Builder addCookie(String key, String value) { + cookies.put(key, value); + return this; + } + + public Builder contentType(String value) { + headers.put("Content-Type", value + ";charset=utf-8"); + return this; + } + + public Builder location(String value) { + headers.put("Location", value); + return this; + } + + public Builder body(byte[] body) { + this.body = body; + return this; + } + + public HttpResponse build() { + return new HttpResponse(protocolVersion, statusCode, statusText, headers, cookies, body); + } + + @Override + public String toString() { + return "Builder{" + + "headers=" + headers + + ", cookies=" + cookies + + ", protocolVersion='" + protocolVersion + '\'' + + ", statusCode=" + statusCode + + ", statusText='" + statusText + '\'' + + ", body=" + Arrays.toString(body) + + '}'; + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Status.java b/tomcat/src/main/java/org/apache/coyote/http11/Status.java new file mode 100644 index 0000000000..86b7b16bb3 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Status.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11; + +public enum Status { + OK(200, "OK"), + FOUND(302, "Found"), + BAD_REQUEST(400, "Bad Request"), + NOT_FOUND(404, "Not Found"), + CONFLICT(409, "Conflict"), + INTERNAL_SERVER_ERROR(500, "Internal Server Error"), + ; + + private final int code; + private final String message; + + Status(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/tomcat/src/test/java/com/techcourse/controller/HomeControllerTest.java b/tomcat/src/test/java/com/techcourse/controller/HomeControllerTest.java new file mode 100644 index 0000000000..613ed030dc --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/controller/HomeControllerTest.java @@ -0,0 +1,36 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.HttpResponse.Builder; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HomeControllerTest { + + @Test + @DisplayName("안녕세상 페이지를 조회한다.") + void viewHomePage() { + // given + RequestMapping requestMapping = new RequestMapping(); + HttpRequest request = HttpRequest.parse(List.of( + "GET / HTTP/1.1" + )); + Builder responseBuilder = HttpResponse.builder(); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 200 OK".getBytes()) + .contains("Content-Type: text/html".getBytes()) + .contains("Content-Length: 12".getBytes()) + .contains("Hello world!".getBytes()); + } +} diff --git a/tomcat/src/test/java/com/techcourse/controller/LoginControllerTest.java b/tomcat/src/test/java/com/techcourse/controller/LoginControllerTest.java new file mode 100644 index 0000000000..c2f8cf8feb --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/controller/LoginControllerTest.java @@ -0,0 +1,99 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.HttpResponse.Builder; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class LoginControllerTest { + + @Test + @DisplayName("로그인 페이지를 조회한다.") + void viewLoginPage() { + // given + RequestMapping requestMapping = new RequestMapping(); + Builder responseBuilder = HttpResponse.builder(); + HttpRequest request = HttpRequest.parse(List.of( + "GET /login HTTP/1.1" + )); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 200 OK".getBytes()) + .contains("Content-Type: text/html".getBytes()) + .contains("로그인".getBytes()); + } + + @Test + @DisplayName("로그인을 하면 홈 화면으로 이동한다.") + void loginAndRedirect() { + // given + RequestMapping requestMapping = new RequestMapping(); + Builder responseBuilder = HttpResponse.builder(); + HttpRequest request = HttpRequest.parse(List.of( + "GET /login?account=gugu&password=password HTTP/1.1" + )); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 302 Found".getBytes()) + .contains("Location: /index.html".getBytes()) + .contains("Set-Cookie: ".getBytes()); + } + + @Test + @DisplayName("존재하지 않는 사용자로 로그인을 하면 401 페이지로 이동한다.") + void loginWithNonAccountAndRedirect() { + // given + RequestMapping requestMapping = new RequestMapping(); + Builder responseBuilder = HttpResponse.builder(); + HttpRequest request = HttpRequest.parse(List.of( + "GET /login?account=seyang&password=password HTTP/1.1" + )); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 302 Found".getBytes()) + .contains("Location: /401.html".getBytes()); + } + + @Test + @DisplayName("잘못된 비밀번호로 로그인을 하면 401 페이지로 이동한다.") + void loginWithDifferentPasswordAndRedirect() { + // given + RequestMapping requestMapping = new RequestMapping(); + Builder responseBuilder = HttpResponse.builder(); + HttpRequest request = HttpRequest.parse(List.of( + "GET /login?account=gugu&password=pw HTTP/1.1" + )); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 302 Found".getBytes()) + .contains("Location: /401.html".getBytes()); + } +} diff --git a/tomcat/src/test/java/com/techcourse/controller/RegisterControllerTest.java b/tomcat/src/test/java/com/techcourse/controller/RegisterControllerTest.java new file mode 100644 index 0000000000..9f87107516 --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/controller/RegisterControllerTest.java @@ -0,0 +1,82 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.HttpResponse.Builder; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RegisterControllerTest { + + @Test + @DisplayName("회원가입 페이지를 조회한다.") + void viewRegisterPage() { + // given + RequestMapping requestMapping = new RequestMapping(); + HttpRequest request = HttpRequest.parse(List.of( + "GET /register HTTP/1.1" + )); + Builder responseBuilder = HttpResponse.builder(); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 200 OK".getBytes()) + .contains("Content-Type: text/html".getBytes()) + .contains("회원가입".getBytes()); + } + + @Test + @DisplayName("회원가입을 한다.") + void register() { + // given + RequestMapping requestMapping = new RequestMapping(); + HttpRequest request = HttpRequest.parse(List.of( + "POST /register HTTP/1.1", + "Content-Type: application/x-www-form-urlencoded", + "", + "account=seyang&email=seyang%40woowa.com&password=pw" + )); + Builder responseBuilder = HttpResponse.builder(); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 302 Found".getBytes()) + .contains("Location: /index.html".getBytes()); + } + + @Test + @DisplayName("일부 정보 없이 회원가입 할 경우 400을 반환한다.") + void registerWithoutInformation() { + // given + RequestMapping requestMapping = new RequestMapping(); + HttpRequest request = HttpRequest.parse(List.of( + "POST /register HTTP/1.1", + "Content-Type: application/x-www-form-urlencoded", + "", + "account=seyang&password=pw" + )); + Builder responseBuilder = HttpResponse.builder(); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 400 Bad Request".getBytes()); + } +}