diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000000..d1724225f3 --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,6 @@ +# 기능 요구 사항 + +## 1단계 - HTTP 서버 구현하기 +- [x] 1\. GET `index.html` 응답하기 +- [x] 2\. CSS 지원하기 +- [x] 3\. Query String 파싱 diff --git a/study/build.gradle b/study/build.gradle index 5c69542f84..7bdce4f751 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -19,7 +19,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'ch.qos.logback:logback-classic:1.5.7' + 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..22a24c36e1 100644 --- a/study/src/main/java/cache/com/example/GreetingController.java +++ b/study/src/main/java/cache/com/example/GreetingController.java @@ -1,11 +1,12 @@ package cache.com.example; +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 { diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheControlInterceptor.java b/study/src/main/java/cache/com/example/cachecontrol/CacheControlInterceptor.java new file mode 100644 index 0000000000..1f3c4d4e0d --- /dev/null +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheControlInterceptor.java @@ -0,0 +1,21 @@ +package cache.com.example.cachecontrol; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.lang.Nullable; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +public class CacheControlInterceptor implements HandlerInterceptor { + + @Override + public void postHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + @Nullable ModelAndView modelAndView + ) { + response.setHeader("Cache-Control", "no-cache, private"); + } +} 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..7959008433 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -9,5 +9,7 @@ public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + registry.addInterceptor(new CacheControlInterceptor()) + .addPathPatterns("/**"); } } 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..9c0f47b0ac 100644 --- a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java +++ b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java @@ -1,12 +1,19 @@ package cache.com.example.etag; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.ShallowEtagHeaderFilter; @Configuration public class EtagFilterConfiguration { -// @Bean -// public FilterRegistrationBean shallowEtagHeaderFilter() { -// return null; -// } + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new ShallowEtagHeaderFilter()); + filterRegistrationBean.addUrlPatterns("/etag"); + filterRegistrationBean.addInitParameter("tag", "etag"); + return filterRegistrationBean; + } } diff --git a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java index 6da6d2c795..1ea48b7848 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -1,7 +1,10 @@ package cache.com.example.version; +import java.time.Duration; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -20,6 +23,8 @@ public CacheBustingWebConfig(ResourceVersion version) { @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") - .addResourceLocations("classpath:/static/"); + .addResourceLocations("classpath:/static/") + .setEtagGenerator(__ -> version.getVersion()) + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic()); } } diff --git a/study/src/main/java/cache/com/example/version/ResourceVersion.java b/study/src/main/java/cache/com/example/version/ResourceVersion.java index 27a2f22813..8c531e3efa 100644 --- a/study/src/main/java/cache/com/example/version/ResourceVersion.java +++ b/study/src/main/java/cache/com/example/version/ResourceVersion.java @@ -2,10 +2,11 @@ import org.springframework.stereotype.Component; -import jakarta.annotation.PostConstruct; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import jakarta.annotation.PostConstruct; + @Component public class ResourceVersion { diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..4c735aea11 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -2,8 +2,12 @@ handlebars: suffix: .html server: + compression: + enabled: true + min-response-size: 10 tomcat: accept-count: 1 max-connections: 1 threads: + min-spare: 2 max: 2 diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..e3f1b0bb88 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,13 +1,14 @@ 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.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; 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 파일을 제공 할 수 있어야 한다. @@ -27,8 +28,9 @@ class FileTest { void resource_디렉터리에_있는_파일의_경로를_찾는다() { final String fileName = "nextstep.txt"; - // todo - final String actual = ""; + final var classLoader = getClass().getClassLoader(); + final var url = classLoader.getResource(fileName); + final String actual = url.toString(); assertThat(actual).endsWith(fileName); } @@ -40,14 +42,14 @@ class FileTest { * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException { final String fileName = "nextstep.txt"; - // todo - final Path path = null; + final var classLoader = getClass().getClassLoader(); + final var url = classLoader.getResource(fileName); + final Path path = Path.of(url.getPath()); - // 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..9c15ad01d1 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,14 +1,26 @@ 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 java.nio.charset.StandardCharsets; + 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)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. @@ -54,6 +66,7 @@ class OutputStream_학습_테스트 { * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ + outputStream.write(bytes); final String actual = outputStream.toString(); assertThat(actual).isEqualTo("nextstep"); @@ -78,6 +91,7 @@ class OutputStream_학습_테스트 { * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); @@ -96,6 +110,7 @@ class OutputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (outputStream) {} verify(outputStream, atLeastOnce()).close(); } @@ -128,7 +143,8 @@ class InputStream_학습_테스트 { * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + byte[] stream = inputStream.readAllBytes(); + final String actual = new String(stream, StandardCharsets.UTF_8); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -148,6 +164,7 @@ class InputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (inputStream) {} verify(inputStream, atLeastOnce()).close(); } @@ -169,12 +186,12 @@ class FilterStream_학습_테스트 { * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? */ @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()); @@ -197,16 +214,23 @@ class InputStreamReader_학습_테스트 { * 필터인 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 var inputStreamReader = new InputStreamReader(inputStream); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader); final StringBuilder actual = new StringBuilder(); + while (bufferedReader.ready()) { + actual.append(bufferedReader.readLine()) + .append("\r\n"); + } + assertThat(actual).hasToString(emoji); } } 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..783bc45a4d 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,17 +1,34 @@ package org.apache.coyote.http11; -import com.techcourse.exception.UncheckedServletException; +import static org.apache.coyote.http11.http.MediaType.DEFAULT_CHARSET; +import static org.apache.coyote.http11.http.MediaType.TEXT_CSS; +import static org.apache.coyote.http11.http.MediaType.TEXT_HTML; + +import java.io.IOException; +import java.net.Socket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + import org.apache.coyote.Processor; +import org.apache.coyote.http11.http.Headers; +import org.apache.coyote.http11.http.HttpRequest; +import org.apache.coyote.http11.http.HttpResponse; +import org.apache.coyote.http11.http.HttpStatusCode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.Socket; +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.exception.UncheckedServletException; public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private static final ClassLoader CLASS_LOADER = ClassLoader.getSystemClassLoader(); + private final Socket connection; public Http11Processor(final Socket connection) { @@ -29,19 +46,65 @@ public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { - final var responseBody = "Hello world!"; + final var request = new HttpRequest(inputStream); + + String body = mapResource(request); - 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 var headers = new Headers(); + headers.put("Content-Type", contentType(request.getAccept())); + headers.put("Content-Type", DEFAULT_CHARSET); + headers.put("Content-Length", String.valueOf(body.getBytes().length)); - outputStream.write(response.getBytes()); + final var httpResponse = new HttpResponse( + request.getHttpVersion(), + HttpStatusCode.OK, + headers, + body + ); + + outputStream.write(httpResponse.getBytes()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } + + private String mapResource(final HttpRequest request) throws IOException { + String body = ""; + if (Objects.equals(request.getPath(), "/")) { + body = "Hello world!"; + } + if (Objects.equals(request.getPath(), "/index.html")) { + var url = CLASS_LOADER.getResource("static" + request.getPath()); + body = Files.readString(Path.of(url.getPath())); + } + if (Objects.equals(request.getPath(), "/css/styles.css")) { + var url = CLASS_LOADER.getResource("static" + request.getPath()); + body = Files.readString(Path.of(url.getPath())); + } + if (Objects.equals(request.getPath(), "/login")) { + var url = CLASS_LOADER.getResource("static" + request.getPath() + ".html"); + body = Files.readString(Path.of(url.getPath())); + String queryString = request.getQueryString(); + if (!queryString.isBlank()) { + Map queryParams = new ConcurrentHashMap<>(); + var params = queryString.split("&"); + for (String param : params) { + String[] keyValue = param.split("="); + queryParams.put(keyValue[0], keyValue[1]); + } + var account = queryParams.get("username"); + var user = InMemoryUserRepository.findByAccount(account).orElse(null); + log.info("user: {}", user); + } + } + return body; + } + + private String contentType(final String accept) { + if (accept != null && accept.contains(TEXT_CSS)) { + return TEXT_CSS; + } + return TEXT_HTML; + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/Headers.java b/tomcat/src/main/java/org/apache/coyote/http11/http/Headers.java new file mode 100644 index 0000000000..2d36ec60eb --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/Headers.java @@ -0,0 +1,45 @@ +package org.apache.coyote.http11.http; + +import java.util.LinkedHashMap; +import java.util.Map; + +public record Headers(Map headers) { + + private static final String DELIMITER = ": "; + + public Headers() { + this(new LinkedHashMap<>()); + } + + public String get(final String name) { + return headers.get(name); + } + + public void put(final String headerLine) { + if (!headerLine.contains(DELIMITER)) { + throw new IllegalArgumentException(headerLine); + } + final var split = headerLine.split(DELIMITER); + put(split[0], split[1]); + } + + public void put(final String name, final String value) { + if (headers.containsKey(name)) { + headers.merge(name, value, (v1, v2) -> v1 + ";" + v2); + return; + } + headers.put(name, value); + } + + @Override + public String toString() { + final var result = new StringBuilder(); + for (String key : headers.keySet()) { + result.append(key) + .append(": ") + .append(headers.get(key)) + .append(" \r\n"); + } + return result.toString(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/http/HttpRequest.java new file mode 100644 index 0000000000..ed036d49ca --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/HttpRequest.java @@ -0,0 +1,85 @@ +package org.apache.coyote.http11.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.StringJoiner; + +public class HttpRequest { + + private final String method; + private final String requestURI; + private final String httpVersion; + private final Headers headers; + private final String body; + + public HttpRequest(final InputStream inputStream) throws IOException { + final var reader = new InputStreamReader(inputStream); + final var buffer = new BufferedReader(reader); + final var requestLine = buffer.readLine(); + final var requestLineSplit = requestLine.split(" "); + this.method = requestLineSplit[0]; + this.requestURI = requestLineSplit[1]; + this.httpVersion = requestLineSplit[2]; + this.headers = createHeaders(buffer); + this.body = createBody(buffer); + } + + private Headers createHeaders(final BufferedReader reader) throws IOException { + final var headers = new Headers(); + var line = reader.readLine(); + while (line != null && !line.isEmpty()) { + headers.put(line); + line = reader.readLine(); + } + return headers; + } + + private String createBody(final BufferedReader reader) throws IOException { + final var body = new StringJoiner("\r\n"); + while (reader.ready()) { + var line = reader.readLine(); + body.add(line); + } + return body.toString(); + } + + public String getPath() { + int index = requestURI.lastIndexOf("?"); + if (index == -1) { + return requestURI; + } + return requestURI.substring(0, index); + } + + public String getQueryString() { + int index = requestURI.lastIndexOf("?"); + if (index == -1) { + return ""; + } + return requestURI.substring(index + 1); + } + + public String getHttpVersion() { + return httpVersion; + } + + public String getAccept() { + return headers.get("Accept"); + } + + public String getBody() { + return body; + } + + @Override + public String toString() { + final var result = new StringJoiner("\r\n"); + final var requestLine = method + " " + requestURI + " " + httpVersion + " "; + return result.add(requestLine) + .add(headers.toString()) + .add(body) + .toString(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/http/HttpResponse.java new file mode 100644 index 0000000000..a52048074b --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/HttpResponse.java @@ -0,0 +1,36 @@ +package org.apache.coyote.http11.http; + +import java.util.StringJoiner; + +public class HttpResponse { + + private final String httpVersion; + private final HttpStatusCode httpStatusCode; + private final Headers headers; + private final String body; + + public HttpResponse(String httpVersion, HttpStatusCode httpStatusCode, Headers headers, String body) { + this.httpVersion = httpVersion; + this.httpStatusCode = httpStatusCode; + this.headers = headers; + this.body = body; + } + + public String getBody() { + return body; + } + + public byte[] getBytes() { + return toString().getBytes(); + } + + @Override + public String toString() { + final var result = new StringJoiner("\r\n"); + final var statusLine = httpVersion + " " + httpStatusCode + " "; + result.add(statusLine) + .add(headers.toString()) + .add(body); + return result.toString(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/HttpStatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/http/HttpStatusCode.java new file mode 100644 index 0000000000..6ed1b100aa --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/HttpStatusCode.java @@ -0,0 +1,23 @@ +package org.apache.coyote.http11.http; + +public enum HttpStatusCode { + + OK(200, "OK"); + + private final int value; + private final String reasonPhrase; + + HttpStatusCode(int value, String reasonPhrase) { + this.value = value; + this.reasonPhrase = reasonPhrase; + } + + public int value() { + return value; + } + + @Override + public String toString() { + return "%s %s".formatted(value, reasonPhrase); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/MediaType.java b/tomcat/src/main/java/org/apache/coyote/http11/http/MediaType.java new file mode 100644 index 0000000000..500ccb301a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/MediaType.java @@ -0,0 +1,8 @@ +package org.apache.coyote.http11.http; + +public class MediaType { + + public static final String DEFAULT_CHARSET = "charset=utf-8"; + public static final String TEXT_HTML = "text/html"; + public static final String TEXT_CSS = "text/css"; +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java index 2aba8c56e0..936768c5b0 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -1,14 +1,15 @@ package org.apache.coyote.http11; -import org.junit.jupiter.api.Test; -import support.StubSocket; +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 static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; + +import support.StubSocket; class Http11ProcessorTest { @@ -35,7 +36,7 @@ void process() { @Test void index() throws IOException { // given - final String httpRequest= String.join("\r\n", + final String httpRequest = String.join("\r\n", "GET /index.html HTTP/1.1 ", "Host: localhost:8080 ", "Connection: keep-alive ", @@ -43,7 +44,7 @@ void index() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final var processor = new Http11Processor(socket); // when processor.process(socket); @@ -53,7 +54,35 @@ void index() throws IOException { 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"+ + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + void css() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /css/styles.css HTTP/1.1 ", + "Host: localhost:8080 ", + "Accept: text/css,*/*;q=0.1 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final var processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/css/styles.css"); + var expected = "HTTP/1.1 200 OK \r\n" + + "Content-Type: text/css;charset=utf-8 \r\n" + + "Content-Length: 211991 \r\n" + + "\r\n" + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); assertThat(socket.output()).isEqualTo(expected);