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/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..e3503a5fb9 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -6,4 +6,5 @@ server: accept-count: 1 max-connections: 1 threads: + min-spare: 2 max: 2 diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..0ab88bbb68 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -4,10 +4,18 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.io.*; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * 자바는 스트림(Stream)으로부터 I/O를 사용한다. @@ -39,7 +47,7 @@ class OutputStream_학습_테스트 { * OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다. * 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, * 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다. - * + * * write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다. * write(byte[] data)write(byte b[], int off, int len) 메서드는 * 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다. @@ -63,7 +71,7 @@ class OutputStream_학습_테스트 { /** * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. - * + * * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 @@ -108,7 +116,7 @@ class OutputStream_학습_테스트 { * InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. * InputStream의 read() 메서드는 기반 메서드이다. * public abstract int read() throws IOException; - * + * * InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다. */ @Nested 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/handler/GetLoginHandler.java b/tomcat/src/main/java/com/techcourse/handler/GetLoginHandler.java new file mode 100644 index 0000000000..18274c6f64 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/handler/GetLoginHandler.java @@ -0,0 +1,25 @@ +package com.techcourse.handler; + +import org.apache.catalina.Manager; +import org.apache.coyote.http11.AbstractHandler; +import org.apache.coyote.http11.ForwardResult; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpStatus; + +import java.net.URI; + +public class GetLoginHandler extends AbstractHandler { + + @Override + public boolean canHandle(HttpRequest httpRequest) { + URI uri = httpRequest.getUri(); + String path = uri.getPath(); + + return "/login".equals(path) && httpRequest.getMethod().isGet(); + } + + @Override + protected ForwardResult forward(HttpRequest httpRequest, Manager sessionManager) { + return new ForwardResult("login.html", HttpStatus.OK); + } +} diff --git a/tomcat/src/main/java/com/techcourse/handler/GetRegisterHandler.java b/tomcat/src/main/java/com/techcourse/handler/GetRegisterHandler.java new file mode 100644 index 0000000000..5f05717cb7 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/handler/GetRegisterHandler.java @@ -0,0 +1,25 @@ +package com.techcourse.handler; + +import org.apache.catalina.Manager; +import org.apache.coyote.http11.AbstractHandler; +import org.apache.coyote.http11.ForwardResult; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpStatus; + +import java.net.URI; + +public class GetRegisterHandler extends AbstractHandler { + + @Override + public boolean canHandle(HttpRequest httpRequest) { + URI uri = httpRequest.getUri(); + String path = uri.getPath(); + + return "/register".equals(path) && httpRequest.getMethod().isGet(); + } + + @Override + protected ForwardResult forward(HttpRequest httpRequest, Manager sessionManager) { + return new ForwardResult("register.html", HttpStatus.OK); + } +} diff --git a/tomcat/src/main/java/com/techcourse/handler/HelloHandler.java b/tomcat/src/main/java/com/techcourse/handler/HelloHandler.java new file mode 100644 index 0000000000..df0c732af4 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/handler/HelloHandler.java @@ -0,0 +1,25 @@ +package com.techcourse.handler; + +import org.apache.catalina.Manager; +import org.apache.coyote.http11.AbstractHandler; +import org.apache.coyote.http11.ForwardResult; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpStatus; + +import java.net.URI; + +public class HelloHandler extends AbstractHandler { + + @Override + public boolean canHandle(HttpRequest httpRequest) { + URI uri = httpRequest.getUri(); + String path = uri.getPath(); + + return "/".equals(path); + } + + @Override + protected ForwardResult forward(HttpRequest httpRequest, Manager sessionManager) { + return new ForwardResult("hello.html", HttpStatus.OK); + } +} diff --git a/tomcat/src/main/java/com/techcourse/handler/NotFoundHandler.java b/tomcat/src/main/java/com/techcourse/handler/NotFoundHandler.java new file mode 100644 index 0000000000..7cbd73605e --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/handler/NotFoundHandler.java @@ -0,0 +1,20 @@ +package com.techcourse.handler; + +import org.apache.catalina.Manager; +import org.apache.coyote.http11.AbstractHandler; +import org.apache.coyote.http11.ForwardResult; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpStatus; + +public class NotFoundHandler extends AbstractHandler { + + @Override + public boolean canHandle(HttpRequest httpRequest) { + return true; + } + + @Override + protected ForwardResult forward(HttpRequest httpRequest, Manager sessionManager) { + return new ForwardResult("404.html", HttpStatus.NOT_FOUND); + } +} diff --git a/tomcat/src/main/java/com/techcourse/handler/PostLoginHandler.java b/tomcat/src/main/java/com/techcourse/handler/PostLoginHandler.java new file mode 100644 index 0000000000..9a6e84b110 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/handler/PostLoginHandler.java @@ -0,0 +1,61 @@ +package com.techcourse.handler; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import jakarta.servlet.http.HttpSession; +import org.apache.catalina.Manager; +import org.apache.coyote.http11.AbstractHandler; +import org.apache.coyote.http11.ForwardResult; +import org.apache.coyote.http11.Header; +import org.apache.coyote.http11.HttpHeaderKey; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpStatus; +import org.apache.coyote.http11.QueryParameter; + +import java.net.URI; + +public class PostLoginHandler extends AbstractHandler { + + @Override + public boolean canHandle(HttpRequest httpRequest) { + URI uri = httpRequest.getUri(); + String path = uri.getPath(); + + return "/login".equals(path) && httpRequest.getMethod().isPost(); + } + + @Override + protected ForwardResult forward(HttpRequest httpRequest, Manager sessionManager) { + if (httpRequest.hasNotApplicationXW3FormUrlEncodedBody()) { + throw new RuntimeException(); + } + + QueryParameter queryParameter = new QueryParameter(httpRequest.body()); + Header header = Header.empty(); + String redirectionPath = "401.html"; + + if (isLoggedIn(queryParameter)) { + HttpSession session = findSessionOrCreate(sessionManager, createCookie(httpRequest)); + session.setAttribute("user", getUser(queryParameter)); + header.append(HttpHeaderKey.SET_COOKIE, getSessionKey() + "=" + session.getId()); + redirectionPath = "index.html"; + } + + header.append(HttpHeaderKey.LOCATION, redirectionPath); + return new ForwardResult(HttpStatus.FOUND, header); + } + + private boolean isLoggedIn(QueryParameter queryParameter) { + String password = queryParameter.get("password").orElse(""); + + return queryParameter.get("account") + .flatMap(InMemoryUserRepository::findByAccount) + .map(it -> it.checkPassword(password)) + .orElse(false); + } + + private User getUser(QueryParameter queryParameter) { + String account = queryParameter.get("account").orElseThrow(); + return InMemoryUserRepository.findByAccount(account).orElseThrow(); + } +} diff --git a/tomcat/src/main/java/com/techcourse/handler/PostRegisterHandler.java b/tomcat/src/main/java/com/techcourse/handler/PostRegisterHandler.java new file mode 100644 index 0000000000..7f0bf64eb7 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/handler/PostRegisterHandler.java @@ -0,0 +1,43 @@ +package com.techcourse.handler; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import org.apache.catalina.Manager; +import org.apache.coyote.http11.AbstractHandler; +import org.apache.coyote.http11.ForwardResult; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpStatus; +import org.apache.coyote.http11.QueryParameter; + +import java.net.URI; + +public class PostRegisterHandler extends AbstractHandler { + + @Override + public boolean canHandle(HttpRequest httpRequest) { + URI uri = httpRequest.getUri(); + String path = uri.getPath(); + + return "/register".equals(path) && httpRequest.getMethod().isPost(); + } + + @Override + protected ForwardResult forward(HttpRequest httpRequest, Manager sessionManager) { + if (httpRequest.hasNotApplicationXW3FormUrlEncodedBody()) { + throw new RuntimeException(); + } + + registerNewUser(httpRequest); + + return new ForwardResult("index.html", HttpStatus.OK); + } + + private void registerNewUser(HttpRequest httpRequest) { + QueryParameter body = new QueryParameter(httpRequest.body()); + String account = body.get("account").orElseThrow(); + String password = body.get("password").orElseThrow(); + String email = body.get("email").orElseThrow(); + + InMemoryUserRepository.save(new User(account, password, email)); + } +} 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..9fed635b96 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.Manager; import org.apache.coyote.http11.Http11Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,15 +18,18 @@ public class Connector implements Runnable { private static final int DEFAULT_ACCEPT_COUNT = 100; private final ServerSocket serverSocket; + private final Manager sessionManager; + private boolean stopped; - public Connector() { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); + public Connector(Manager sessionManager) { + this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, sessionManager); } - public Connector(final int port, final int acceptCount) { + public Connector(final int port, final int acceptCount, final Manager sessionManager) { this.serverSocket = createServerSocket(port, acceptCount); this.stopped = false; + this.sessionManager = sessionManager; } private ServerSocket createServerSocket(final int port, final int acceptCount) { @@ -66,7 +70,7 @@ private void process(final Socket connection) { if (connection == null) { return; } - var processor = new Http11Processor(connection); + var processor = new Http11Processor(connection, sessionManager); new Thread(processor).start(); } diff --git a/tomcat/src/main/java/org/apache/catalina/session/SimpleSession.java b/tomcat/src/main/java/org/apache/catalina/session/SimpleSession.java new file mode 100644 index 0000000000..bcfca52a58 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/SimpleSession.java @@ -0,0 +1,101 @@ +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; +import java.util.UUID; + +public class SimpleSession implements HttpSession { + + private final UUID sessionId = UUID.randomUUID(); + private final Map values = new HashMap<>(); + + @Override + public long getCreationTime() { + return 0; + } + + @Override + public String getId() { + return sessionId.toString(); + } + + @Override + public long getLastAccessedTime() { + return 0; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public void setMaxInactiveInterval(int interval) { + + } + + @Override + public int getMaxInactiveInterval() { + return 0; + } + + @Override + public HttpSessionContext getSessionContext() { + return null; + } + + @Override + public Object getAttribute(String name) { + return values.get(name); + } + + @Override + public Object getValue(String name) { + return null; + } + + @Override + public Enumeration getAttributeNames() { + return null; + } + + @Override + public String[] getValueNames() { + return new String[0]; + } + + @Override + public void setAttribute(String name, Object value) { + values.put(name, value); + } + + @Override + public void putValue(String name, Object value) { + + } + + @Override + public void removeAttribute(String name) { + + } + + @Override + public void removeValue(String name) { + + } + + @Override + public void invalidate() { + + } + + @Override + public boolean isNew() { + return false; + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/session/SimpleSessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SimpleSessionManager.java new file mode 100644 index 0000000000..9df39bc49f --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/SimpleSessionManager.java @@ -0,0 +1,27 @@ +package org.apache.catalina.session; + +import jakarta.servlet.http.HttpSession; +import org.apache.catalina.Manager; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class SimpleSessionManager implements Manager { + + private static final Map SESSIONS = new ConcurrentHashMap<>(); + + @Override + public void add(HttpSession session) { + SESSIONS.put(session.getId(), session); + } + + @Override + public HttpSession findSession(String id) { + return SESSIONS.get(id); + } + + @Override + public void remove(HttpSession session) { + SESSIONS.remove(session.getId()); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java index 205159e95b..8e1bb30c8a 100644 --- a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java +++ b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java @@ -1,6 +1,8 @@ package org.apache.catalina.startup; +import org.apache.catalina.Manager; import org.apache.catalina.connector.Connector; +import org.apache.catalina.session.SimpleSessionManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,8 +12,10 @@ public class Tomcat { private static final Logger log = LoggerFactory.getLogger(Tomcat.class); + private final Manager manager = new SimpleSessionManager(); + public void start() { - var connector = new Connector(); + var connector = new Connector(manager); connector.start(); try { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/AbstractHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/AbstractHandler.java new file mode 100644 index 0000000000..1a4b5d08e8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/AbstractHandler.java @@ -0,0 +1,69 @@ +package org.apache.coyote.http11; + +import jakarta.servlet.http.HttpSession; +import org.apache.catalina.Manager; +import org.apache.catalina.session.SimpleSession; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; + +public abstract class AbstractHandler { + + public abstract boolean canHandle(HttpRequest httpRequest); + + protected abstract ForwardResult forward(HttpRequest httpRequest, Manager sessionManager); + + public HttpResponse handle(HttpRequest httpRequest, Manager sessionManager) { + ForwardResult result = forward(httpRequest, sessionManager); + String resourcePath = getClass().getClassLoader().getResource("static/" + result.path()).getPath(); + Header header = result.header(); + String contentType = ContentType.determineContentType(resourcePath).getName(); + header.append(HttpHeaderKey.CONTENT_TYPE, contentType); + + if (result.httpStatus().isRedirection()) { + return new HttpResponse(httpRequest.getHttpVersion(), result.httpStatus(), header, new byte[]{}); + } + + return new HttpResponse( + httpRequest.getHttpVersion(), + result.httpStatus(), + header, + readStaticResource(resourcePath) + ); + } + + private byte[] readStaticResource(String resourcePath) { + try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(resourcePath))) { + return bufferedInputStream.readAllBytes(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected Cookie createCookie(HttpRequest httpRequest) { + return httpRequest.header() + .get(HttpHeaderKey.COOKIE) + .map(Cookie::new) + .orElse(new Cookie("")); + } + + protected HttpSession findSessionOrCreate(Manager sessionManager, Cookie cookie) { + String id = cookie.get(getSessionKey()).orElse(""); + if (id.isBlank()) { + HttpSession session = new SimpleSession(); + sessionManager.add(session); + return session; + } + + try { + return sessionManager.findSession(id); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected String getSessionKey() { + return "JSESSIONID"; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java new file mode 100644 index 0000000000..93f9261da9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java @@ -0,0 +1,52 @@ +package org.apache.coyote.http11; + +import java.util.Arrays; +import java.util.NoSuchElementException; + +public enum ContentType { + + HTML("text/html", ".html"), + JS("text/javascript", ".js"), + CSS("text/css", ".css"), + SVG("image/svg+xml", ".svg"), + PLAIN("text/plain", ""), + APPLICATION_X_WWW_FORM_URL_ENCODED("application/x-www-form-urlencoded", ""), + ; + + private final String name; + private final String extension; + + ContentType(String name, String extension) { + this.name = name; + this.extension = extension; + } + + public static ContentType from(String contentTypeName) { + return Arrays.stream(values()) + .filter(it -> it.name.equals(contentTypeName)) + .findFirst() + .orElseThrow(NoSuchElementException::new); + } + + public static ContentType determineContentType(String resourcePath) { + for (ContentType contentType : ContentType.values()) { + if (resourcePath.endsWith(contentType.getExtension())) { + return contentType; + } + } + + return ContentType.PLAIN; + } + + public boolean isApplicationXW3FormUrlEncoded() { + return this.equals(APPLICATION_X_WWW_FORM_URL_ENCODED); + } + + public String getName() { + return name + ";charset=utf-8"; + } + + public String getExtension() { + return extension; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Cookie.java b/tomcat/src/main/java/org/apache/coyote/http11/Cookie.java new file mode 100644 index 0000000000..24c818a68a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Cookie.java @@ -0,0 +1,46 @@ +package org.apache.coyote.http11; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class Cookie { + + private static final String PAIR_DELIMITER = "="; + private static final String COOKIE_DELIMITER = ";"; + + private final Map cookie = new HashMap<>(); + + public Cookie(String cookie) { + if (cookie == null) { + cookie = ""; + } + + parseCookie(cookie); + } + + private void parseCookie(String cookie) { + String[] pairs = cookie.split(COOKIE_DELIMITER); + + for (String pair : pairs) { + if (pair.contains(PAIR_DELIMITER)) { + String[] keyValuePair = pair.trim().split(PAIR_DELIMITER); + putIfValidPair(keyValuePair); + } + } + } + + private void putIfValidPair(String[] keyValuePair) { + if (keyValuePair.length != 2) { + return; + } + String key = keyValuePair[0].trim(); + String value = keyValuePair[1].trim(); + + cookie.put(key, value); + } + + public Optional get(String key) { + return Optional.ofNullable(cookie.get(key)); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ForwardResult.java b/tomcat/src/main/java/org/apache/coyote/http11/ForwardResult.java new file mode 100644 index 0000000000..041519a4a5 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/ForwardResult.java @@ -0,0 +1,12 @@ +package org.apache.coyote.http11; + +public record ForwardResult(String path, HttpStatus httpStatus, Header header) { + + public ForwardResult(HttpStatus httpStatus, Header header) { + this("", httpStatus, header); + } + + public ForwardResult(String path, HttpStatus httpStatus) { + this(path, httpStatus, Header.empty()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Header.java b/tomcat/src/main/java/org/apache/coyote/http11/Header.java new file mode 100644 index 0000000000..59e2ea1330 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Header.java @@ -0,0 +1,61 @@ +package org.apache.coyote.http11; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class Header { + + private static final String PAIR_DELIMITER = ":"; + + private final Map header = new HashMap<>(); + + public static Header empty() { + return new Header(Collections.emptyList()); + } + + public Header(List header) { + parseHeader(header); + } + + private void parseHeader(List header) { + for (String pair : header) { + if (pair.contains(PAIR_DELIMITER)) { + String[] split = pair.split(PAIR_DELIMITER); + putIfValidPair(split); + } + } + } + + private void putIfValidPair(String[] keyValuePair) { + if (keyValuePair.length != 2) { + return; + } + String key = keyValuePair[0].trim(); + String value = keyValuePair[1].trim(); + + append(key, value); + } + + public void append(HttpHeaderKey key, String value) { + append(key.getName(), value); + } + + public void append(String key, String value) { + header.put(key, value); + } + + public Optional get(HttpHeaderKey key) { + return get(key.getName()); + } + + public Optional get(String key) { + return Optional.ofNullable(header.get(key)); + } + + public Map getHeader() { + return Collections.unmodifiableMap(header); + } +} 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..39c12e12d6 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,21 +1,36 @@ package org.apache.coyote.http11; import com.techcourse.exception.UncheckedServletException; +import com.techcourse.handler.GetLoginHandler; +import com.techcourse.handler.GetRegisterHandler; +import com.techcourse.handler.HelloHandler; +import com.techcourse.handler.NotFoundHandler; +import com.techcourse.handler.PostLoginHandler; +import com.techcourse.handler.PostRegisterHandler; +import org.apache.catalina.Manager; import org.apache.coyote.Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.Socket; +import java.util.ArrayList; +import java.util.List; public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); private final Socket connection; + private final Manager sessionManager; - public Http11Processor(final Socket connection) { + public Http11Processor(Socket connection, Manager sessionManager) { this.connection = connection; + this.sessionManager = sessionManager; } @Override @@ -25,23 +40,68 @@ public void run() { } @Override - public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { - - 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); - - outputStream.write(response.getBytes()); + public void process(Socket connection) { + try (InputStream inputStream = connection.getInputStream(); + OutputStream outputStream = connection.getOutputStream()) { + HttpRequest httpRequest = createHttpRequest(inputStream); + HttpResponse httpResponse = respondResource(httpRequest); + outputStream.write(httpResponse.serialize()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } + + private HttpRequest createHttpRequest(InputStream inputStream) throws IOException { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + String requestLine = bufferedReader.readLine(); + Header header = createHeader(bufferedReader); + char[] requestBody = createRequestBody(bufferedReader, header); + + return new HttpRequest(requestLine, header, requestBody); + } + + private Header createHeader(BufferedReader bufferedReader) throws IOException { + List headerTokens = new ArrayList<>(); + String line = bufferedReader.readLine(); + while (!line.isBlank()) { + line = bufferedReader.readLine(); + headerTokens.add(line); + } + + return new Header(headerTokens); + } + + private char[] createRequestBody(BufferedReader bufferedReader, Header header) throws IOException { + int length = Integer.parseInt(header.get(HttpHeaderKey.CONTENT_LENGTH.getName()).orElse("0")); + char[] requestBody = new char[length]; + bufferedReader.read(requestBody); + + return requestBody; + } + + private HttpResponse respondResource(HttpRequest httpRequest) throws IOException { + AbstractHandler helloHandler = new HelloHandler(); + AbstractHandler staticResourceHandler = new StaticResourceHandler(); + AbstractHandler postLoginHandler = new PostLoginHandler(); + AbstractHandler postRegisterHandler = new PostRegisterHandler(); + AbstractHandler getLoginHandler = new GetLoginHandler(); + AbstractHandler getRegisterHandler = new GetRegisterHandler(); + AbstractHandler notFoundHandler = new NotFoundHandler(); + + List handlers = List.of( + helloHandler, + staticResourceHandler, + postLoginHandler, + postRegisterHandler, + getLoginHandler, + getRegisterHandler + ); + AbstractHandler targetHandler = handlers.stream() + .filter(it -> it.canHandle(httpRequest)) + .findFirst() + .orElse(notFoundHandler); + + return targetHandler.handle(httpRequest, sessionManager); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaderKey.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaderKey.java new file mode 100644 index 0000000000..5358b55d41 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaderKey.java @@ -0,0 +1,21 @@ +package org.apache.coyote.http11; + +public enum HttpHeaderKey { + + CONTENT_LENGTH("Content-Length"), + CONTENT_TYPE("Content-Type"), + LOCATION("Location"), + COOKIE("Cookie"), + SET_COOKIE("Set-Cookie"), + ; + + private final String name; + + HttpHeaderKey(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java new file mode 100644 index 0000000000..42d95eaa60 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java @@ -0,0 +1,26 @@ +package org.apache.coyote.http11; + +import java.util.Arrays; +import java.util.NoSuchElementException; + +public enum HttpMethod { + + GET, + POST, + ; + + public static HttpMethod from(String value) { + return Arrays.stream(values()) + .filter(it -> it.name().equals(value)) + .findFirst() + .orElseThrow(NoSuchElementException::new); + } + + public boolean isGet() { + return this.equals(GET); + } + + public boolean isPost() { + return this.equals(POST); + } +} 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..609a093390 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java @@ -0,0 +1,30 @@ +package org.apache.coyote.http11; + +import java.net.URI; + +public record HttpRequest(String startLine, Header header, char[] body) { + + public HttpMethod getMethod() { + return HttpMethod.from(startLine.split(" ")[0]); + } + + public URI getUri() { + return URI.create(startLine.split(" ")[1]); + } + + public HttpVersion getHttpVersion() { + return HttpVersion.from(startLine.split(" ")[2]); + } + + public QueryParameter getQueryParameter() { + return new QueryParameter(getUri().getQuery()); + } + + public boolean hasNotApplicationXW3FormUrlEncodedBody() { + String requestContentType = header.get(HttpHeaderKey.CONTENT_TYPE) + .orElse(ContentType.PLAIN.getName()); + ContentType contentType = ContentType.from(requestContentType); + + return !contentType.isApplicationXW3FormUrlEncoded(); + } +} 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..b45fb685d0 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java @@ -0,0 +1,36 @@ +package org.apache.coyote.http11; + +import java.util.Map; + +public record HttpResponse( + HttpVersion httpVersion, + HttpStatus httpStatus, + Header header, + byte[] responseBody +) { + + private static final String RESPONSE_HEADER_FORMAT = "%s: %s \r\n"; + + public byte[] serialize() { + String message = String.join("\r\n", getStartLine(), getHeaders(), getBody()); + return message.getBytes(); + } + + private String getStartLine() { + return httpVersion.getVersionName() + " " + httpStatus.getDescription() + " "; + } + + private CharSequence getHeaders() { + StringBuilder stringBuilder = new StringBuilder(); + Map headerMap = header.getHeader(); + headerMap.forEach((key, value) -> stringBuilder.append(String.format(RESPONSE_HEADER_FORMAT, key, value))); + String format = String.format(RESPONSE_HEADER_FORMAT, HttpHeaderKey.CONTENT_LENGTH.getName(), responseBody.length); + stringBuilder.append(format); + + return stringBuilder; + } + + private String getBody() { + return new String(responseBody); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java new file mode 100644 index 0000000000..b59f9d4873 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java @@ -0,0 +1,25 @@ +package org.apache.coyote.http11; + +public enum HttpStatus { + + OK(200, "OK"), + FOUND(302, "Found"), + NOT_FOUND(404, "Not Found"), + ; + + private final int code; + private final String name; + + HttpStatus(int code, String name) { + this.code = code; + this.name = name; + } + + public String getDescription() { + return code + " " + name; + } + + public boolean isRedirection() { + return code >= 300 && code < 400; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpVersion.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpVersion.java new file mode 100644 index 0000000000..1d147c3d80 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpVersion.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11; + +import java.util.Arrays; +import java.util.NoSuchElementException; + +enum HttpVersion { + + HTTP_1_1("HTTP/1.1"), + ; + + private final String versionName; + + HttpVersion(String versionName) { + this.versionName = versionName; + } + + public static HttpVersion from(String versionName) { + return Arrays.stream(values()) + .filter(it -> it.versionName.equals(versionName)) + .findFirst() + .orElseThrow(NoSuchElementException::new); + } + + public String getVersionName() { + return versionName; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/QueryParameter.java b/tomcat/src/main/java/org/apache/coyote/http11/QueryParameter.java new file mode 100644 index 0000000000..6acffc7aca --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/QueryParameter.java @@ -0,0 +1,58 @@ +package org.apache.coyote.http11; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class QueryParameter { + + private static final String PAIR_DELIMITER = "="; + private static final String PARAMETER_DELIMITER = "&"; + + private final Map queryParameter = new HashMap<>(); + + public QueryParameter(char[] body) { + this(new String(body)); + } + + public QueryParameter(String queryParameter) { + if (queryParameter == null) { + queryParameter = ""; + } + + parseQueryParameter(queryParameter); + } + + private void parseQueryParameter(String queryParameter) { + String[] pairs = queryParameter.split(PARAMETER_DELIMITER); + + for (String pair : pairs) { + if (pair.contains(PAIR_DELIMITER)) { + String[] keyValuePair = pair.split(PAIR_DELIMITER); + putIfValidPair(keyValuePair); + } + } + } + + private void putIfValidPair(String[] keyValuePair) { + if (keyValuePair.length != 2) { + return; + } + String key = keyValuePair[0]; + String value = keyValuePair[1]; + + queryParameter.put(key, value); + } + + public Optional get(String key) { + return Optional.ofNullable(queryParameter.get(key)); + } + + public boolean isEmpty() { + return queryParameter.isEmpty(); + } + + public int getSize() { + return queryParameter.size(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceHandler.java new file mode 100644 index 0000000000..7847637e41 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceHandler.java @@ -0,0 +1,23 @@ +package org.apache.coyote.http11; + +import org.apache.catalina.Manager; + +import java.net.URI; +import java.net.URL; + +public class StaticResourceHandler extends AbstractHandler { + + @Override + public boolean canHandle(HttpRequest httpRequest) { + URL resource = getClass().getClassLoader().getResource("static/" + httpRequest.getUri()); + + return resource != null; + } + + @Override + public ForwardResult forward(HttpRequest httpRequest, Manager sessionManager) { + URI uri = httpRequest.getUri(); + + return new ForwardResult(uri.getPath(), HttpStatus.OK); + } +} diff --git a/tomcat/src/main/resources/static/assets/img/error-404-monochrome.svg b/tomcat/src/main/resources/static/assets/img/error-404-monochrome.svg index f0d345f991..04464ecd0a 100644 --- a/tomcat/src/main/resources/static/assets/img/error-404-monochrome.svg +++ b/tomcat/src/main/resources/static/assets/img/error-404-monochrome.svg @@ -1 +1 @@ -error-404-monochrome \ No newline at end of file +error-404-monochrome \ No newline at end of file diff --git a/tomcat/src/main/resources/static/hello.html b/tomcat/src/main/resources/static/hello.html new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/tomcat/src/main/resources/static/hello.html @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..bc933357f2 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -20,7 +20,7 @@

로그인

-
+
diff --git a/tomcat/src/test/java/com/techcourse/handler/GetLoginHandlerTest.java b/tomcat/src/test/java/com/techcourse/handler/GetLoginHandlerTest.java new file mode 100644 index 0000000000..ae6206c19c --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/handler/GetLoginHandlerTest.java @@ -0,0 +1,35 @@ +package com.techcourse.handler; + +import org.apache.coyote.http11.Header; +import org.apache.coyote.http11.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class GetLoginHandlerTest { + + @Test + @DisplayName("로그인 관련 GET 요청을 처리할 수 있다.") + void canHandle() { + GetLoginHandler getLoginHandler = new GetLoginHandler(); + + boolean result = getLoginHandler.canHandle(createHttpRequest("GET /login HTTP/1.1")); + + assertThat(result).isTrue(); + } + + @Test + @DisplayName("로그인 관련 GET 요청이 아니라면 처리할 수 없다.") + void cantHandle() { + GetLoginHandler getLoginHandler = new GetLoginHandler(); + + boolean result = getLoginHandler.canHandle(createHttpRequest("POST /login HTTP/1.1")); + + assertThat(result).isFalse(); + } + + private HttpRequest createHttpRequest(String startLine) { + return new HttpRequest(startLine, Header.empty(), "".toCharArray()); + } +} diff --git a/tomcat/src/test/java/com/techcourse/handler/GetRegisterHandlerTest.java b/tomcat/src/test/java/com/techcourse/handler/GetRegisterHandlerTest.java new file mode 100644 index 0000000000..9403d4771a --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/handler/GetRegisterHandlerTest.java @@ -0,0 +1,35 @@ +package com.techcourse.handler; + +import org.apache.coyote.http11.Header; +import org.apache.coyote.http11.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class GetRegisterHandlerTest { + + @Test + @DisplayName("회원가입 관련 GET 요청을 처리할 수 있다.") + void canHandle() { + GetRegisterHandler getRegisterHandler = new GetRegisterHandler(); + + boolean result = getRegisterHandler.canHandle(createHttpRequest("GET /register HTTP/1.1")); + + assertThat(result).isTrue(); + } + + @Test + @DisplayName("회원가입 관련 GET 요청이 아니라면 처리할 수 없다.") + void cantHandle() { + GetRegisterHandler getRegisterHandler = new GetRegisterHandler(); + + boolean result = getRegisterHandler.canHandle(createHttpRequest("POST /register HTTP/1.1")); + + assertThat(result).isFalse(); + } + + private HttpRequest createHttpRequest(String startLine) { + return new HttpRequest(startLine, Header.empty(), "".toCharArray()); + } +} diff --git a/tomcat/src/test/java/com/techcourse/handler/HelloHandlerTest.java b/tomcat/src/test/java/com/techcourse/handler/HelloHandlerTest.java new file mode 100644 index 0000000000..65bb20ca2a --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/handler/HelloHandlerTest.java @@ -0,0 +1,35 @@ +package com.techcourse.handler; + +import org.apache.coyote.http11.Header; +import org.apache.coyote.http11.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class HelloHandlerTest { + + @Test + @DisplayName("루트 경로 요청을 처리할 수 있다.") + void canHandle() { + HelloHandler helloHandler = new HelloHandler(); + + boolean result = helloHandler.canHandle(createHttpRequest("GET / HTTP/1.1")); + + assertThat(result).isTrue(); + } + + @Test + @DisplayName("루트 경로가 아닌 요청은 처리할 수 없다.") + void cantHandle() { + HelloHandler helloHandler = new HelloHandler(); + + boolean result = helloHandler.canHandle(createHttpRequest("GET /login HTTP/1.1")); + + assertThat(result).isFalse(); + } + + private HttpRequest createHttpRequest(String startLine) { + return new HttpRequest(startLine, Header.empty(), "".toCharArray()); + } +} diff --git a/tomcat/src/test/java/com/techcourse/handler/PostLoginHandlerTest.java b/tomcat/src/test/java/com/techcourse/handler/PostLoginHandlerTest.java new file mode 100644 index 0000000000..e98f832c5e --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/handler/PostLoginHandlerTest.java @@ -0,0 +1,35 @@ +package com.techcourse.handler; + +import org.apache.coyote.http11.Header; +import org.apache.coyote.http11.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PostLoginHandlerTest { + + @Test + @DisplayName("로그인 관련 POST 요청을 처리할 수 있다.") + void canHandle() { + PostLoginHandler postLoginHandler = new PostLoginHandler(); + + boolean result = postLoginHandler.canHandle(createHttpRequest("POST /login HTTP/1.1")); + + assertThat(result).isTrue(); + } + + @Test + @DisplayName("로그인 관련 POST 요청이 아니라면 처리할 수 없다.") + void cantHandle() { + PostLoginHandler postLoginHandler = new PostLoginHandler(); + + boolean result = postLoginHandler.canHandle(createHttpRequest("GET /login HTTP/1.1")); + + assertThat(result).isFalse(); + } + + private HttpRequest createHttpRequest(String startLine) { + return new HttpRequest(startLine, Header.empty(), "".toCharArray()); + } +} diff --git a/tomcat/src/test/java/com/techcourse/handler/PostRegisterHandlerTest.java b/tomcat/src/test/java/com/techcourse/handler/PostRegisterHandlerTest.java new file mode 100644 index 0000000000..9f29fd37ce --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/handler/PostRegisterHandlerTest.java @@ -0,0 +1,35 @@ +package com.techcourse.handler; + +import org.apache.coyote.http11.Header; +import org.apache.coyote.http11.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PostRegisterHandlerTest { + + @Test + @DisplayName("회원가입 관련 POST 요청을 처리할 수 있다.") + void canHandle() { + PostRegisterHandler postRegisterHandler = new PostRegisterHandler(); + + boolean result = postRegisterHandler.canHandle(createHttpRequest("POST /register HTTP/1.1")); + + assertThat(result).isTrue(); + } + + @Test + @DisplayName("회원가입 관련 POST 요청이 아니라면 처리할 수 없다.") + void cantHandle() { + PostRegisterHandler postRegisterHandler = new PostRegisterHandler(); + + boolean result = postRegisterHandler.canHandle(createHttpRequest("GET /register HTTP/1.1")); + + assertThat(result).isFalse(); + } + + private HttpRequest createHttpRequest(String startLine) { + return new HttpRequest(startLine, Header.empty(), "".toCharArray()); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/ContentTypeTest.java b/tomcat/src/test/java/org/apache/coyote/http11/ContentTypeTest.java new file mode 100644 index 0000000000..53fec2ef9c --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/ContentTypeTest.java @@ -0,0 +1,29 @@ +package org.apache.coyote.http11; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class ContentTypeTest { + + @ParameterizedTest + @CsvSource( + value = { + "index.html:text/html", + "style.css:text/css", + "sample.js:text/javascript", + "image.svg:image/svg+xml", + "hi.unknown:text/plain" + }, + delimiter = ':' + ) + @DisplayName("주어진 확장자에 맞는 content-type을 반환한다.") + void determineContentType(String source, String expected) { + ContentType contentType = ContentType.determineContentType(source); + + assertThat(contentType.getName()).startsWith(expected); + assertThat(contentType.getName()).endsWith(";charset=utf-8"); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/CookieTest.java b/tomcat/src/test/java/org/apache/coyote/http11/CookieTest.java new file mode 100644 index 0000000000..be659c88df --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/CookieTest.java @@ -0,0 +1,52 @@ +package org.apache.coyote.http11; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +class CookieTest { + + @Test + @DisplayName("쿠키 문자열은 null이 될 수 있다.") + void createWithNull() { + assertThatCode(() -> new Cookie(null)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("쿠키는 a=2 형식만 인식된다.") + void singleFormat() { + Cookie cookie = new Cookie("a:2"); + + Optional result = cookie.get("a"); + + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("단일 쿠키를 조회한다.") + void single() { + Cookie cookie = new Cookie("a=2"); + + Optional result = cookie.get("a"); + + assertThat(result).hasValue("2"); + } + + @Test + @DisplayName("여러 쿠키값을 읽는다.") + void multi() { + Cookie cookie = new Cookie("a=1; b=2; c=3"); + + Assertions.assertAll( + () -> assertThat(cookie.get("a")).hasValue("1"), + () -> assertThat(cookie.get("b")).hasValue("2"), + () -> assertThat(cookie.get("c")).hasValue("3") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/HeaderTest.java b/tomcat/src/test/java/org/apache/coyote/http11/HeaderTest.java new file mode 100644 index 0000000000..2e784060e1 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/HeaderTest.java @@ -0,0 +1,78 @@ +package org.apache.coyote.http11; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class HeaderTest { + + @Test + @DisplayName("헤더는 a:b 형식만 인식한다.") + void singleFormat() { + Header header = new Header(List.of("a-2")); + + Optional result = header.get("a"); + + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("단일 헤더를 조회한다.") + void single() { + Header header = new Header(List.of("a:2")); + + Optional result = header.get("a"); + + assertThat(result).hasValue("2"); + } + + @Test + @DisplayName("여러 헤더를 읽는다.") + void multi() { + List headers = List.of("a:1", "b:2", "c:3"); + Header header = new Header(headers); + + Assertions.assertAll( + () -> assertThat(header.get("a")).hasValue("1"), + () -> assertThat(header.get("b")).hasValue("2"), + () -> assertThat(header.get("c")).hasValue("3") + ); + } + + @Test + @DisplayName("header 상수를 조회 키로 사용할 수 있다.") + void enumKey() { + Header header = new Header(List.of("Location:/admin")); + + Optional result = header.get(HttpHeaderKey.LOCATION); + + assertThat(result).hasValue("/admin"); + } + + @Test + @DisplayName("정의된 헤더를 추가할 수 있다.") + void append() { + Header header = Header.empty(); + + header.append(HttpHeaderKey.LOCATION, "/admin"); + + Optional result = header.get(HttpHeaderKey.LOCATION); + assertThat(result).hasValue("/admin"); + } + + @Test + @DisplayName("사용자 정의 헤더를 추가할 수 있다.") + void appendCustom() { + Header header = Header.empty(); + + header.append("a", "b"); + + Optional result = header.get("a"); + assertThat(result).hasValue("b"); + } +} 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..242c646ed1 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,8 @@ package org.apache.coyote.http11; +import org.apache.catalina.Manager; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import support.StubSocket; import java.io.File; @@ -16,7 +18,7 @@ class Http11ProcessorTest { void process() { // given final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); + final var processor = new Http11Processor(socket, Mockito.mock(Manager.class)); // when processor.process(socket); @@ -43,7 +45,7 @@ void index() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final Http11Processor processor = new Http11Processor(socket, Mockito.mock(Manager.class)); // when processor.process(socket); diff --git a/tomcat/src/test/java/org/apache/coyote/http11/HttpStatusTest.java b/tomcat/src/test/java/org/apache/coyote/http11/HttpStatusTest.java new file mode 100644 index 0000000000..460de30cb6 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/HttpStatusTest.java @@ -0,0 +1,19 @@ +package org.apache.coyote.http11; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class HttpStatusTest { + + @Test + @DisplayName("응답 메시지에 들어갈 문자열을 생성한다.") + void message() { + Assertions.assertAll( + () -> assertThat(HttpStatus.OK.getDescription()).isEqualTo("200 OK"), + () -> assertThat(HttpStatus.FOUND.getDescription()).isEqualTo("302 Found") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/QueryParameterTest.java b/tomcat/src/test/java/org/apache/coyote/http11/QueryParameterTest.java new file mode 100644 index 0000000000..dc65777052 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/QueryParameterTest.java @@ -0,0 +1,66 @@ +package org.apache.coyote.http11; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class QueryParameterTest { + + @Test + @DisplayName("쿼리 파라미터 문자열은 null이 될 수 있다.") + void createWithNull() { + QueryParameter queryParameter = new QueryParameter((String) null); + + assertThat(queryParameter.isEmpty()).isTrue(); + } + + @Test + @DisplayName("키값 페어는 a=b 형식만 인식한다.") + void checkFormat() { + QueryParameter queryParameter = new QueryParameter("a-2"); + + assertThat(queryParameter.isEmpty()).isTrue(); + } + + @Test + @DisplayName("단일 키값 페어를 조회한다.") + void single() { + QueryParameter queryParameter = new QueryParameter("a=2"); + + Optional result = queryParameter.get("a"); + + assertThat(result).hasValue("2"); + } + + @Test + @DisplayName("여러 키값 페어를 읽는다.") + void multi() { + List strings = List.of("a=1", "b=2", "c=3"); + String input = String.join("&", strings); + QueryParameter queryParameter = new QueryParameter(input); + + Assertions.assertAll( + () -> assertThat(queryParameter.get("a")).hasValue("1"), + () -> assertThat(queryParameter.get("b")).hasValue("2"), + () -> assertThat(queryParameter.get("c")).hasValue("3") + ); + } + + @ParameterizedTest + @CsvSource(value = {"=&:0", "a:0", "a=1:1", "a=2&b=2:2", "a=2&a=2:1", "&a=1&:1", "a=1&b=2&c=3:3"}, delimiter = ':') + @DisplayName("여러 표현식을 처리한다.") + void parametrized(String source, int expectedSize) { + QueryParameter queryParameter = new QueryParameter(source); + + int result = queryParameter.getSize(); + + assertThat(result).isEqualTo(expectedSize); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/StaticResourceHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/http11/StaticResourceHandlerTest.java new file mode 100644 index 0000000000..11f4140d80 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/StaticResourceHandlerTest.java @@ -0,0 +1,33 @@ +package org.apache.coyote.http11; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class StaticResourceHandlerTest { + + @Test + @DisplayName("static 하위에 존재하는 리소스라면 정적 리소스 핸들러가 처리할 수 있다.") + void canHandle() { + StaticResourceHandler staticResourceHandler = new StaticResourceHandler(); + + boolean result = staticResourceHandler.canHandle(createHttpRequest("GET /sample.txt HTTP/1.1")); + + assertThat(result).isTrue(); + } + + @Test + @DisplayName("static 하위에 존재하지 않는 리소스라면 정적 리소스 핸들러가 처리할 수 없다.") + void cantHandle() { + StaticResourceHandler staticResourceHandler = new StaticResourceHandler(); + + boolean result = staticResourceHandler.canHandle(createHttpRequest("GET /unknown.txt HTTP/1.1")); + + assertThat(result).isFalse(); + } + + private HttpRequest createHttpRequest(String startLine) { + return new HttpRequest(startLine, Header.empty(), "".toCharArray()); + } +} diff --git a/tomcat/src/test/resources/static/sample.txt b/tomcat/src/test/resources/static/sample.txt new file mode 100644 index 0000000000..e69de29bb2