diff --git a/study/build.gradle b/study/build.gradle index 5c69542f84..87a1f0313c 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -19,7 +19,6 @@ 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.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..5ec03b4a35 100644 --- a/study/src/main/java/cache/com/example/GreetingController.java +++ b/study/src/main/java/cache/com/example/GreetingController.java @@ -12,7 +12,7 @@ public class GreetingController { @GetMapping("/") public String index() { - return "index"; + return "index.html"; } /** @@ -25,12 +25,12 @@ public String cacheControl(final HttpServletResponse response) { .cachePrivate() .getHeaderValue(); response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControl); - return "index"; + return "index.html"; } @GetMapping("/etag") public String etag() { - return "index"; + return "index.html"; } @GetMapping("/resource-versioning") 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..e413dd2926 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,13 +1,19 @@ package cache.com.example.cachecontrol; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.WebContentInterceptor; @Configuration public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + CacheControl cacheControl = CacheControl.noCache().cachePrivate(); + WebContentInterceptor webContentInterceptor = new WebContentInterceptor(); + webContentInterceptor.addCacheMapping(cacheControl, "/**"); + registry.addInterceptor(webContentInterceptor); } } diff --git a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java index 41ef7a3d9a..db9507bb93 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<>( new ShallowEtagHeaderFilter()); + filterRegistrationBean.addUrlPatterns("/etag"); + filterRegistrationBean.addUrlPatterns("/resources/*"); + return filterRegistrationBean; + } } diff --git a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java index 6da6d2c795..c7addcdab4 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,9 @@ 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 +22,7 @@ public CacheBustingWebConfig(ResourceVersion version) { @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") - .addResourceLocations("classpath:/static/"); + .addResourceLocations("classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic()); } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..8b74bdfd88 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -6,4 +6,8 @@ server: accept-count: 1 max-connections: 1 threads: + min-spare: 2 max: 2 + compression: + enabled: true + min-response-size: 10 diff --git a/study/src/main/resources/static/index.html b/study/src/main/resources/static/index.html new file mode 100644 index 0000000000..46cbef0f24 --- /dev/null +++ b/study/src/main/resources/static/index.html @@ -0,0 +1,10 @@ + + + + Getting Started: Serving Web Content + + + +Hello, World! + + diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..d37c48f16f 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,10 +1,13 @@ package study; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.nio.file.Path; -import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -28,7 +31,7 @@ class FileTest { final String fileName = "nextstep.txt"; // todo - final String actual = ""; + final String actual = getClass().getClassLoader().getResource(fileName).getPath(); assertThat(actual).endsWith(fileName); } @@ -40,14 +43,15 @@ class FileTest { * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException { final String fileName = "nextstep.txt"; + final URL url = getClass().getClassLoader().getResource(fileName); // todo - final Path path = null; + final Path path = new File(url.getFile()).toPath(); // 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..ff36c86863 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -53,6 +53,7 @@ class OutputStream_학습_테스트 { * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ + outputStream.write(bytes); final String actual = outputStream.toString(); @@ -78,6 +79,7 @@ class OutputStream_학습_테스트 { * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); @@ -96,6 +98,8 @@ class OutputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (outputStream) { + } verify(outputStream, atLeastOnce()).close(); } @@ -128,7 +132,7 @@ class InputStream_학습_테스트 { * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + final String actual = new String(inputStream.readAllBytes()); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -148,6 +152,8 @@ class InputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (inputStream) { + } verify(inputStream, atLeastOnce()).close(); } @@ -169,12 +175,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,15 +203,20 @@ 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 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); final StringBuilder actual = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + actual.append(line).append("\r\n"); + } assertThat(actual).hasToString(emoji); } diff --git a/tomcat/build.gradle b/tomcat/build.gradle index 21063b298f..ccb3e2d92d 100644 --- a/tomcat/build.gradle +++ b/tomcat/build.gradle @@ -22,6 +22,7 @@ dependencies { testImplementation 'org.mockito:mockito-core:5.12.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' } test { diff --git a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java index d3fa57feeb..10a79ae83c 100644 --- a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java @@ -23,5 +23,9 @@ public static Optional findByAccount(String account) { return Optional.ofNullable(database.get(account)); } + public static void delete(User user) { + database.remove(user.getAccount()); + } + private InMemoryUserRepository() {} } diff --git a/tomcat/src/main/java/com/techcourse/model/User.java b/tomcat/src/main/java/com/techcourse/model/User.java index e8cf4c8e68..675b6463ca 100644 --- a/tomcat/src/main/java/com/techcourse/model/User.java +++ b/tomcat/src/main/java/com/techcourse/model/User.java @@ -8,6 +8,10 @@ public class User { private final String email; public User(Long id, String account, String password, String email) { + validateAccount(account); + validatePassword(password); + validateEmail(email); + this.id = id; this.account = account; this.password = password; @@ -18,6 +22,24 @@ public User(String account, String password, String email) { this(null, account, password, email); } + private void validateAccount(String account) { + if (account == null || account.isBlank()) { + throw new IllegalArgumentException("account는 비어 있을 수 없습니다."); + } + } + + private void validatePassword(String password) { + if (password == null || password.isBlank()) { + throw new IllegalArgumentException("password는 비어 있을 수 없습니다."); + } + } + + private void validateEmail(String email) { + if (email == null || email.isBlank()) { + throw new IllegalArgumentException("email은 비어 있을 수 없습니다."); + } + } + public boolean checkPassword(String password) { return this.password.equals(password); } 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..4490fe7945 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -1,5 +1,6 @@ package org.apache.catalina.connector; +import org.apache.catalina.session.UuidSessionGenerator; import org.apache.coyote.http11.Http11Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,7 +67,7 @@ private void process(final Socket connection) { if (connection == null) { return; } - var processor = new Http11Processor(connection); + var processor = new Http11Processor(connection, new UuidSessionGenerator()); new Thread(processor).start(); } diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/session/Manager.java similarity index 89% rename from tomcat/src/main/java/org/apache/catalina/Manager.java rename to tomcat/src/main/java/org/apache/catalina/session/Manager.java index e69410f6a9..c94fdd7650 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/session/Manager.java @@ -1,6 +1,4 @@ -package org.apache.catalina; - -import jakarta.servlet.http.HttpSession; +package org.apache.catalina.session; import java.io.IOException; @@ -29,7 +27,7 @@ public interface Manager { * * @param session Session to be added */ - void add(HttpSession session); + void add(Session session); /** * Return the active Session, associated with this Manager, with the @@ -45,12 +43,12 @@ public interface Manager { * @return the request session or {@code null} if a session with the * requested ID could not be found */ - HttpSession findSession(String id) throws IOException; + Session findSession(String id); /** * Remove this Session from the active Sessions for this Manager. * * @param session Session to be removed */ - void remove(HttpSession session); + void remove(Session session); } diff --git a/tomcat/src/main/java/org/apache/catalina/session/Session.java b/tomcat/src/main/java/org/apache/catalina/session/Session.java new file mode 100644 index 0000000000..68df752718 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/Session.java @@ -0,0 +1,38 @@ +package org.apache.catalina.session; + +import java.util.HashMap; +import java.util.Map; + +public class Session { + + private final String id; + private final Map values = new HashMap<>(); + + public Session(String id) { + this.id = id; + } + + public boolean contains(String name) { + return getAttribute(name) != null; + } + + public Object getAttribute(String name) { + return values.get(name); + } + + public void setAttribute(String name, Object value) { + values.put(name, value); + } + + public void removeAttribute(String name) { + values.remove(name); + } + + public void invalidate() { + values.clear(); + } + + public String getId() { + return id; + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionGenerator.java b/tomcat/src/main/java/org/apache/catalina/session/SessionGenerator.java new file mode 100644 index 0000000000..059856be39 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionGenerator.java @@ -0,0 +1,6 @@ +package org.apache.catalina.session; + +@FunctionalInterface +public interface SessionGenerator { + Session create(); +} 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..c14f1703a0 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -0,0 +1,27 @@ +package org.apache.catalina.session; + +import java.util.HashMap; +import java.util.Map; + +public class SessionManager implements Manager { + + private static final Map SESSIONS = new HashMap<>(); + + @Override + public void add(Session session) { + SESSIONS.put(session.getId(), session); + } + + @Override + public Session findSession(String id) { + if (id == null) { + return null; + } + return SESSIONS.get(id); + } + + @Override + public void remove(Session session) { + SESSIONS.remove(session.getId()); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/session/UuidSessionGenerator.java b/tomcat/src/main/java/org/apache/catalina/session/UuidSessionGenerator.java new file mode 100644 index 0000000000..99546dff57 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/UuidSessionGenerator.java @@ -0,0 +1,12 @@ +package org.apache.catalina.session; + +import java.util.UUID; + +public class UuidSessionGenerator implements SessionGenerator { + + @Override + public Session create() { + String sessionId = UUID.randomUUID().toString(); + return new Session(sessionId); + } +} 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..5616af0387 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,7 +1,27 @@ package org.apache.coyote.http11; +import com.techcourse.db.InMemoryUserRepository; import com.techcourse.exception.UncheckedServletException; +import com.techcourse.model.User; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionGenerator; +import org.apache.catalina.session.SessionManager; import org.apache.coyote.Processor; +import org.apache.coyote.http11.request.HttpMethod; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.Queries; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpStatus; +import org.apache.coyote.http11.response.ResponseCookie; +import org.apache.coyote.http11.response.ResponseFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,11 +31,19 @@ public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private static final Set SERVING_PATHS = Set.of( + "/login", + "/register", + "/index" + ); + private final static SessionManager SESSION_MANAGER = new SessionManager(); private final Socket connection; + private final SessionGenerator sessionGenerator; - public Http11Processor(final Socket connection) { + public Http11Processor(Socket connection, SessionGenerator sessionGenerator) { this.connection = connection; + this.sessionGenerator = sessionGenerator; } @Override @@ -26,22 +54,126 @@ public void run() { @Override public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { + try (InputStream inputStream = connection.getInputStream(); + OutputStream outputStream = connection.getOutputStream()) { - final var responseBody = "Hello world!"; + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + HttpRequest request = HttpRequest.of(bufferedReader); + HttpResponse response = getResponse(request); + String formattedResponse = response.toResponse(); - 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); - - outputStream.write(response.getBytes()); + outputStream.write(formattedResponse.getBytes()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } + + private HttpResponse getResponse(HttpRequest request) throws IOException { + String requestPath = request.getPath(); + if (requestPath.equals("/login") && request.getMethod() == HttpMethod.GET && isLoginUser(request)) { + return HttpResponse.createRedirectResponse(HttpStatus.FOUND, "/index.html"); + } + if (SERVING_PATHS.contains(requestPath) && request.isQueriesEmpty() && request.getMethod() == HttpMethod.GET) { + requestPath += ".html"; + } + if (requestPath.contains(".")) { + return getFileResponse(requestPath); + } + if (requestPath.equals("/login") && request.getMethod() == HttpMethod.POST) { + return login(request); + } + if (requestPath.equals("/register") && request.getMethod() == HttpMethod.POST) { + return register(request); + } + throw new IllegalArgumentException("요청을 처리할 수 없습니다."); + } + + private boolean isLoginUser(HttpRequest request) { + return request.getSessionId() + .map(this::isLoginUser) + .orElse(false); + } + + private boolean isLoginUser(String sessionId) { + Session session = SESSION_MANAGER.findSession(sessionId); + if (session == null) { + return false; + } + return session.contains("user"); + } + + private HttpResponse getFileResponse(String requestPath) { + try { + URL resource = getClass().getClassLoader().getResource("static/" + requestPath); + if (resource == null) { + throw new IllegalArgumentException("존재하지 않는 자원입니다: " + requestPath); + } + ResponseFile responseFile = ResponseFile.of(resource); + return HttpResponse.createFileResponse(responseFile); + } catch (Exception e) { + log.error(e.getMessage(), e); + return HttpResponse.createRedirectResponse( + HttpStatus.FOUND, + "/404.html" + ); + } + } + + private HttpResponse register(HttpRequest request) { + Queries queries = Queries.of(request.getBody()); + String account = queries.get("account"); + + User user = new User(account, queries.get("password"), queries.get("email")); + + if (InMemoryUserRepository.findByAccount(account).isPresent()) { + String message = "이미 존재하는 사용자입니다: " + account; + log.warn(message); + return HttpResponse.createTextResponse(HttpStatus.CONFLICT, message); + } + InMemoryUserRepository.save(user); + return HttpResponse.createRedirectResponse(HttpStatus.FOUND, "/index.html"); + } + + private HttpResponse login(HttpRequest request) { + String requestBody = request.getBody(); + Queries queries = Queries.of(requestBody); + String account = queries.get("account"); + String password = queries.get("password"); + validateLoginRequest(account, password); + + try { + User user = getLoginUser(account, password); + Session session = sessionGenerator.create(); + session.setAttribute("user", user); + SESSION_MANAGER.add(session); + ResponseCookie sessionCookie = ResponseCookie.of(session); + Map headers = new HashMap<>(); + headers.put("Location", "/index.html"); + headers.put("Set-Cookie", sessionCookie.toResponse()); + return new HttpResponse(HttpStatus.FOUND, headers); + } catch (Exception e) { + log.warn(e.getMessage(), e); + return HttpResponse.createRedirectResponse(HttpStatus.FOUND, "/401.html"); + } + } + + private User getLoginUser(String account, String password) { + User user = InMemoryUserRepository.findByAccount(account) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + + if (!user.checkPassword(password)) { + throw new IllegalArgumentException("잘못된 비밀번호입니다."); + } + return user; + } + + private void validateLoginRequest(String account, String password) { + if (account == null || account.isEmpty()) { + throw new IllegalArgumentException("account는 비어 있을 수 없습니다."); + } + if (password == null || password.isEmpty()) { + throw new IllegalArgumentException("password는 비어 있을 수 없습니다."); + } + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpHeaders.java new file mode 100644 index 0000000000..20395524eb --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpHeaders.java @@ -0,0 +1,57 @@ +package org.apache.coyote.http11.request; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +public class HttpHeaders { + + private static final String HEADER_SEPARATOR = ": "; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + + private final Map values; + + public HttpHeaders(Map values) { + this.values = values; + } + + public static HttpHeaders of(List headers) { + Map map = new HashMap<>(); + for (String header : headers) { + if (!isValidHeader(header)) { + continue; + } + String[] splitHeader = header.split(HEADER_SEPARATOR); + map.put(splitHeader[KEY_INDEX], splitHeader[VALUE_INDEX]); + } + return new HttpHeaders(map); + } + + private static boolean isValidHeader(String header) { + List splitHeader = Arrays.stream(header.split(HEADER_SEPARATOR)).toList(); + if (splitHeader.size() != 2) { + return false; + } + return splitHeader.stream() + .noneMatch(String::isBlank); + } + + public String get(String key) { + return values.get(key); + } + + public OptionalInt getAsInt(String key) { + String value = get(key); + if (value == null) { + return OptionalInt.empty(); + } + try { + return OptionalInt.of(Integer.parseInt(value)); + } catch (NumberFormatException e) { + return OptionalInt.empty(); + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java new file mode 100644 index 0000000000..fb34be1c00 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java @@ -0,0 +1,19 @@ +package org.apache.coyote.http11.request; + +import java.util.Arrays; + +public enum HttpMethod { + GET, + POST; + + private boolean isSameMethod(String method) { + return name().equals(method); + } + + public static HttpMethod getByName(String method) { + return Arrays.stream(values()) + .filter(httpMethod -> httpMethod.isSameMethod(method)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 HTTP 메서드입니다.")); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java new file mode 100644 index 0000000000..4da7e0498f --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,95 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class HttpRequest { + + private final RequestLine requestLine; + private final HttpHeaders headers; + private final String body; + + public HttpRequest(RequestLine requestLine, HttpHeaders headers, String body) { + this.requestLine = requestLine; + this.headers = headers; + this.body = body; + } + + public static HttpRequest of(BufferedReader requestReader) throws IOException { + List requestHead = readRequestHead(requestReader); + validateRequestHead(requestHead); + RequestLine requestLine = getRequestLine(requestHead); + HttpHeaders headers = getHeaders(requestHead); + + int contentLength = headers.getAsInt("Content-Length").orElse(0); + String body = readBody(contentLength, requestReader); + + return new HttpRequest(requestLine, headers, body); + } + + private static List readRequestHead(BufferedReader requestReader) throws IOException { + List requestHead = new ArrayList<>(); + String line; + while ((line = requestReader.readLine()) != null && !line.isEmpty()) { + requestHead.add(line); + } + return requestHead; + } + + private static void validateRequestHead(List requestLines) { + if (requestLines.isEmpty()) { + throw new IllegalArgumentException("올바르지 않은 HTTP 요청 형식입니다."); + } + } + + private static RequestLine getRequestLine(List requestHead) { + String firstLine = requestHead.getFirst(); + return RequestLine.of(firstLine); + } + + private static HttpHeaders getHeaders(List requestHead) { + List headers = new ArrayList<>(requestHead.subList(1, requestHead.size())); + return HttpHeaders.of(headers); + } + + private static String readBody(int contentLength, BufferedReader requestReader) throws IOException { + if (contentLength <= 0) { + return ""; + } + char[] body = new char[contentLength]; + requestReader.read(body, 0, contentLength); + return new String(body); + } + + public String getPath() { + return requestLine.getPath(); + } + + public Queries getQueries() { + return requestLine.getQueries(); + } + + public boolean isQueriesEmpty() { + return getQueries().isEmpty(); + } + + public HttpMethod getMethod() { + return requestLine.getMethod(); + } + + public String getBody() { + return body; + } + + public Optional getSessionId() { + RequestCookies requestCookies = RequestCookies.of(headers.get("Cookie")); + String sessionId = requestCookies.get("JSESSIONID"); + if (sessionId == null) { + return Optional.empty(); + } + return Optional.of(sessionId); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Queries.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Queries.java new file mode 100644 index 0000000000..431efd3bdd --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/Queries.java @@ -0,0 +1,46 @@ +package org.apache.coyote.http11.request; + +import java.util.HashMap; +import java.util.Map; + +public class Queries { + + public static final Queries EMPTY_QUERIES = new Queries(Map.of()); + private static final String QUERIES_SEPARATOR = "&"; + private static final String QUERY_SEPARATOR = "="; + + private final Map values; + + public Queries(Map values) { + this.values = values; + } + + public static Queries of(String queries) { + String[] splitQueries = queries.split(QUERIES_SEPARATOR); + Map map = new HashMap<>(); + for (String query : splitQueries) { + if (!isValidQuery(query)) { + continue; + } + int index = query.indexOf(QUERY_SEPARATOR); + String key = query.substring(0, index); + String value = query.substring(index + 1); + map.put(key, value); + } + return new Queries(map); + } + + private static boolean isValidQuery(String query) { + int index = query.indexOf(QUERY_SEPARATOR); + int queryLastIndex = query.length() - 1; + return index != -1 && index != queryLastIndex; + } + + public boolean isEmpty() { + return values.isEmpty(); + } + + public String get(String key) { + return values.get(key); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java new file mode 100644 index 0000000000..dcff0149c7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java @@ -0,0 +1,45 @@ +package org.apache.coyote.http11.request; + +import java.util.HashMap; +import java.util.Map; + +public class RequestCookies { + + private static final RequestCookies EMPTY_COOKIES = new RequestCookies(Map.of()); + private static final String COOKIES_SEPARATOR = ";"; + private static final String COOKIE_SEPARATOR = "="; + + private final Map properties; + + public RequestCookies(Map values) { + this.properties = values; + } + + public static RequestCookies of(String httpCookies) { + if (httpCookies == null || httpCookies.isBlank()) { + return EMPTY_COOKIES; + } + String[] splitHttpCookies = httpCookies.split(COOKIES_SEPARATOR); + Map cookies = new HashMap<>(); + for (String cookie : splitHttpCookies) { + if (!isValidCookie(cookie)) { + continue; + } + int index = cookie.indexOf(COOKIE_SEPARATOR); + String key = cookie.substring(0, index).trim(); + String value = cookie.substring(index + 1).trim(); + cookies.put(key, value); + } + return new RequestCookies(cookies); + } + + private static boolean isValidCookie(String cookie) { + int index = cookie.indexOf(COOKIE_SEPARATOR); + int cookieLastIndex = cookie.length() - 1; + return index != -1 && index != cookieLastIndex; + } + + public String get(String key) { + return properties.get(key); + } +} 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..0e7a405cea --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java @@ -0,0 +1,61 @@ +package org.apache.coyote.http11.request; + +import java.util.Arrays; +import java.util.List; + +public class RequestLine { + + private static final int SPILT_REQUEST_LINE_COUNT = 3; + private static final int METHOD_INDEX = 0; + private static final int URI_INDEX = 1; + private static final int PROTOCOL_INDEX = 2; + + private final HttpMethod method; + private final RequestUri requestUri; + private final String protocol; + + public RequestLine(HttpMethod method, RequestUri requestUri, String protocol) { + this.method = method; + this.requestUri = requestUri; + this.protocol = protocol; + } + + public static RequestLine of(String requestLine) { + List splitRequestLine = Arrays.stream(requestLine.split(" ")).toList(); + if (splitRequestLine.size() != SPILT_REQUEST_LINE_COUNT) { + throw new IllegalArgumentException("올바르지 않은 request line 형식입니다."); + } + + return new RequestLine( + getMethod(splitRequestLine), + getUri(splitRequestLine), + splitRequestLine.get(PROTOCOL_INDEX) + ); + } + + private static HttpMethod getMethod(List splitRequestLine) { + String methodName = splitRequestLine.get(METHOD_INDEX); + return HttpMethod.getByName(methodName); + } + + private static RequestUri getUri(List splitRequestLine) { + String uri = splitRequestLine.get(URI_INDEX); + return RequestUri.of(uri); + } + + public HttpMethod getMethod() { + return method; + } + + public String getPath() { + return requestUri.getPath(); + } + + public Queries getQueries() { + return requestUri.getQueries(); + } + + public String getProtocol() { + return protocol; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestUri.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestUri.java new file mode 100644 index 0000000000..cfe47d8092 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestUri.java @@ -0,0 +1,38 @@ +package org.apache.coyote.http11.request; + +public class RequestUri { + + private static final String QUERY_START = "?"; + + private final String path; + private final Queries queries; + + public RequestUri(String path, Queries queries) { + this.path = path; + this.queries = queries; + } + + public static RequestUri of(String uri) { + boolean hasQueries = hasQueries(uri); + if (!hasQueries) { + return new RequestUri(uri, Queries.EMPTY_QUERIES); + } + int queryStartIndex = uri.indexOf(QUERY_START); + String path = uri.substring(0, queryStartIndex); + String queryString = uri.substring(queryStartIndex + 1); + return new RequestUri(path, Queries.of(queryString)); + } + + private static boolean hasQueries(String uri) { + int queryStartIndex = uri.indexOf(QUERY_START); + return queryStartIndex != -1 && queryStartIndex != uri.length() - 1; + } + + public String getPath() { + return path; + } + + public Queries getQueries() { + return queries; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java new file mode 100644 index 0000000000..9bdfafea66 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,102 @@ +package org.apache.coyote.http11.response; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class HttpResponse { + + private static final String LINE_SEPARATOR = "\r\n"; + private static final String STATUS_LINE_FORMAT = "%s %d %s "; + private static final String HEADER_FORMAT = "%s: %s "; + + private final Protocol protocol = Protocol.HTTP11; + private final HttpStatus httpStatus; + private final Map headers; + private final String responseBody; + + public HttpResponse(HttpStatus httpStatus, Map headers) { + this(httpStatus, headers, ""); + } + + public HttpResponse(HttpStatus httpStatus, Map headers, String responseBody) { + this.httpStatus = httpStatus; + this.headers = headers; + this.responseBody = responseBody; + } + + public static HttpResponse createRedirectResponse(HttpStatus httpStatus, String location) { + Map headers = new HashMap<>(); + headers.put("Location", location); + return new HttpResponse( + httpStatus, + headers + ); + } + + public static HttpResponse createTextResponse(HttpStatus httpStatus, String responseBody) { + Map headers = new HashMap<>(); + int contentLength = responseBody.getBytes().length; + headers.put("Content-Type", "text/plain;charset=utf-8 "); + headers.put("Content-Length", String.valueOf(contentLength)); + + return new HttpResponse( + httpStatus, + headers, + responseBody + ); + } + + public static HttpResponse createFileResponse(ResponseFile responseFile) { + Map headers = new HashMap<>(); + String responseBody = responseFile.getContent(); + int contentLength = responseBody.getBytes().length; + headers.put("Content-Type", responseFile.getContentType()); + headers.put("Content-Length", String.valueOf(contentLength)); + + return new HttpResponse( + HttpStatus.OK, + headers, + responseBody + ); + } + + public String toResponse() { + String statusLine = getStatusLine(); + String headers = getHeaders(); + + return String.join( + LINE_SEPARATOR, + statusLine, + headers, + "", + responseBody + ); + } + + private String getStatusLine() { + return String.format(STATUS_LINE_FORMAT, + protocol.getName(), + httpStatus.getCode(), + httpStatus.getReasonPhrase() + ); + } + + private String getHeaders() { + List formattedHeaders = new ArrayList<>(); + for (String headerKey : headers.keySet()) { + String headerValue = headers.get(headerKey); + String formattedHeader = String.format(HEADER_FORMAT, headerKey, headerValue); + formattedHeaders.add(formattedHeader); + } + return String.join(LINE_SEPARATOR, formattedHeaders); + } + +// public void addSession(Session session) { +// if (headers.containsKey("JSESSIONID")) { +// return; +// } +// headers.put("JSESSIONID", session.getId()); +// } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java new file mode 100644 index 0000000000..8d31b9e449 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java @@ -0,0 +1,25 @@ +package org.apache.coyote.http11.response; + +public enum HttpStatus { + OK(200, "OK"), + FOUND(302, "Found"), + UNAUTHORIZED(401, "Unauthorized"), + NOT_FOUND(404, "Not Found"), + CONFLICT(409, "Conflict"); + + private final int code; + private final String reasonPhrase; + + HttpStatus(int code, String reasonPhrase) { + this.code = code; + this.reasonPhrase = reasonPhrase; + } + + public int getCode() { + return code; + } + + public String getReasonPhrase() { + return reasonPhrase; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/Protocol.java b/tomcat/src/main/java/org/apache/coyote/http11/response/Protocol.java new file mode 100644 index 0000000000..7bb9fb17b3 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/Protocol.java @@ -0,0 +1,15 @@ +package org.apache.coyote.http11.response; + +public enum Protocol { + HTTP11("HTTP/1.1"); + + private final String name; + + Protocol(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseCookie.java new file mode 100644 index 0000000000..53bbc43b3a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseCookie.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.response; + +import org.apache.catalina.session.Session; + +public class ResponseCookie { + + private static final String COOKIE_FORMAT = "%s=%s"; + + private final String name; + private final String value; + + public ResponseCookie(String name, String value) { + this.name = name; + this.value = value; + } + + public static ResponseCookie of(Session session) { + return new ResponseCookie("JSESSIONID", session.getId()); + } + + public String toResponse() { + return String.format(COOKIE_FORMAT, name, value); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseFile.java b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseFile.java new file mode 100644 index 0000000000..2a1f02a235 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseFile.java @@ -0,0 +1,35 @@ +package org.apache.coyote.http11.response; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ResponseFile { + + private final String contentType; + private final String content; + + public ResponseFile(String contentType, String content) { + this.contentType = contentType; + this.content = content; + } + + public static ResponseFile of(URL resource) throws IOException { + File resourceFile = new File(resource.getFile()); + Path resourceFilePath = resourceFile.toPath(); + String contentType = Files.probeContentType(resourceFilePath) + ";charset=utf-8"; + String responseBody = new String(Files.readAllBytes(resourceFilePath)); + + return new ResponseFile(contentType, responseBody); + } + + public String getContentType() { + return contentType; + } + + public String getContent() { + return content; + } +} diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..457b36255a 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -10,6 +10,7 @@ +
@@ -20,7 +21,7 @@

로그인

-
+
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..26f6a91b7a 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -1,6 +1,16 @@ package org.apache.coyote.http11; +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import org.apache.catalina.session.Session; +import org.apache.catalina.session.UuidSessionGenerator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import support.StubSocket; import java.io.File; @@ -9,53 +19,175 @@ import java.nio.file.Files; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; class Http11ProcessorTest { - @Test - void process() { - // given - final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); + @Nested + class ResourceFileTest { + @DisplayName("특정 엔드포인트에 대한 올바른 HTML 파일을 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"/index", "/login", "/register"}) + void getResourceHtml(String requestPath) throws IOException { + String httpRequest = String.join("\r\n", + "GET " + requestPath + " HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); - // when - processor.process(socket); + StubSocket socket = new StubSocket(httpRequest); + Http11Processor processor = new Http11Processor(socket, new UuidSessionGenerator()); - // then - var expected = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: 12 ", - "", - "Hello world!"); + processor.process(socket); - assertThat(socket.output()).isEqualTo(expected); + String responseBody = getFile(requestPath + ".html"); + int contentLength = responseBody.getBytes().length; + + assertThat(socket.output()).contains( + "HTTP/1.1 200 OK \r\n", + "Content-Type: text/html;charset=utf-8 \r\n", + "Content-Length: " + contentLength + " \r\n", + responseBody + ); + } + + @DisplayName("CSS 파일을 반환한다.") + @Test + void getCssFile() throws IOException { + String httpRequest = String.join("\r\n", + "GET /css/styles.css HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + StubSocket socket = new StubSocket(httpRequest); + Http11Processor processor = new Http11Processor(socket, new UuidSessionGenerator()); + + processor.process(socket); + + String responseBody = getFile("/css/styles.css"); + int contentLength = responseBody.getBytes().length; + + assertThat(socket.output()).contains( + "HTTP/1.1 200 OK \r\n", + "Content-Type: text/css;charset=utf-8 \r\n", + "Content-Length: " + contentLength + " \r\n", + responseBody + ); + } + + private String getFile(String fileName) throws IOException { + URL resource = getClass().getClassLoader().getResource("static/" + fileName); + return new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + } + } + + @Nested + class LoginTest { + + private final String account = "account"; + private final String password = "password"; + private final User user = new User(account, password, "aaa@gmail.com"); + + @BeforeEach + void saveUser() { + InMemoryUserRepository.save(user); + } + + @AfterEach + void deleteUser() { + InMemoryUserRepository.delete(user); + } + + @DisplayName("로그인 성공 시 올바르게 응답을 보낸다.") + @Test + void loginSuccess() { + String sessionId = "sessionId"; + Session session = new Session(sessionId); + String body = String.format("account=%s&password=%s", account, password); + int contentLength = body.getBytes().length; + String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + contentLength, + "", + body); + + StubSocket socket = new StubSocket(httpRequest); + Http11Processor processor = new Http11Processor(socket, () -> session); + + processor.process(socket); + + assertThat(socket.output()).contains( + "HTTP/1.1 302 Found \r\n", + "Location: /index.html \r\n", + "Set-Cookie: JSESSIONID=" + sessionId + ); + } + + @DisplayName("로그인 실패 시 올바르게 응답을 보낸다.") + @Test + void loginFail() { + String sessionId = "sessionId"; + Session session = new Session(sessionId); + String body = String.format("account=%s&password=%s", "invalid account", "password"); + int contentLength = body.getBytes().length; + String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + contentLength, + "", + body); + + StubSocket socket = new StubSocket(httpRequest); + Http11Processor processor = new Http11Processor(socket, () -> session); + + processor.process(socket); + + assertThat(socket.output()).contains( + "HTTP/1.1 302 Found \r\n", + "Location: /401.html \r\n" + ); + } } - @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); + @Nested + class RegisterTest { + + @DisplayName("회원 가입 시 올바르게 응답을 보낸다.") + @Test + void register() { + String sessionId = "sessionId"; + String account = "account"; + User user = new User(account, "password", "kkk@gmail.com"); + Session session = new Session(sessionId); + String body = "account=account&password=password&email=kkk@gmail.com"; + int contentLength = body.getBytes().length; + String httpRequest = String.join("\r\n", + "POST /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + contentLength, + "", + body); + + StubSocket socket = new StubSocket(httpRequest); + Http11Processor processor = new Http11Processor(socket, () -> session); + + processor.process(socket); + + assertAll( + () -> assertThat(socket.output()).contains( + "HTTP/1.1 302 Found \r\n", + "Location: /index.html \r\n" + ), + () -> assertThat(InMemoryUserRepository.findByAccount(account)).isNotEmpty() + ); + InMemoryUserRepository.delete(user); + } } } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java new file mode 100644 index 0000000000..70a6002b52 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java @@ -0,0 +1,32 @@ +package org.apache.coyote.http11.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HttpRequestTest { + + @DisplayName("요청 쿼리 스트링을 파싱한다.") + @Test + void parseQuery() throws IOException { + String rawRequest = String.join("\r\n", + "GET /subway?bread=white&sauce=pepper HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + BufferedReader requestReader = new BufferedReader(new StringReader(rawRequest)); + + Queries queries = HttpRequest.of(requestReader).getQueries(); + + assertAll( + () -> assertThat(queries.get("bread")).isEqualTo("white"), + () -> assertThat(queries.get("sauce")).isEqualTo("pepper") + ); + } +}