diff --git a/study/build.gradle b/study/build.gradle index 5c69542f84..bb8b420ca5 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -19,10 +19,10 @@ 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' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.assertj:assertj-core:3.26.0' 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..b29cdb1aec 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,21 @@ 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..35e972aeae 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,20 @@ 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; + +import cache.com.example.version.CacheBustingWebConfig; @Configuration public class EtagFilterConfiguration { -// @Bean -// public FilterRegistrationBean shallowEtagHeaderFilter() { -// return null; -// } + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean filter = new FilterRegistrationBean<>(); + filter.setFilter(new ShallowEtagHeaderFilter()); + filter.addUrlPatterns("/etag", CacheBustingWebConfig.PREFIX_STATIC_RESOURCES + "/*"); + return filter; + } } 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..8cf8162a46 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -1,7 +1,10 @@ package cache.com.example.version; +import java.time.Duration; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -20,6 +23,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..4c735aea11 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -2,8 +2,12 @@ handlebars: suffix: .html server: + compression: + enabled: true + min-response-size: 10 tomcat: accept-count: 1 max-connections: 1 threads: + min-spare: 2 max: 2 diff --git a/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..41d4b152e9 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) throws IOException; /** * 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..82cdf13ea9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/Session.java @@ -0,0 +1,35 @@ +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(final String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public Object getAttribute(final String name) { + return values.entrySet() + .stream() + .filter(entry -> entry.getKey().equals(name)) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + } + + public Map getValues() { + return values; + } + + public void setAttribute(String name, Object value) { + values.put(name, value); + } +} 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..0f4423a4d9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -0,0 +1,35 @@ +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<>(); + private static final SessionManager INSTANCE = new SessionManager(); + + private SessionManager() {} + + public static SessionManager getInstance() { + return INSTANCE; + } + + public boolean existsById(final String id) { + return SESSIONS.containsKey(id); + } + + @Override + public void add(Session session) { + SESSIONS.put(session.getId(), session); + } + + @Override + public Session findSession(final String id) { + return SESSIONS.get(id); + } + + @Override + public void remove(Session session) { + SESSIONS.remove(session.getId()); + } +} + diff --git a/tomcat/src/main/java/org/apache/coyote/HandlerMapping.java b/tomcat/src/main/java/org/apache/coyote/HandlerMapping.java new file mode 100644 index 0000000000..2312982139 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/HandlerMapping.java @@ -0,0 +1,59 @@ +package org.apache.coyote; + +import org.apache.coyote.handler.Handler; +import org.apache.coyote.handler.LoginHandler; +import org.apache.coyote.handler.RegisterHandler; +import org.apache.coyote.handler.RootEndPointHandler; +import org.apache.coyote.handler.StaticResourceHandler; +import org.apache.coyote.handler.exception.InternalServerErrorHandler; +import org.apache.coyote.handler.exception.NotFoundHandler; +import org.apache.coyote.handler.exception.UnAuthorizationHandler; +import org.apache.http.request.HttpRequest; + + +public class HandlerMapping { + + private static final HandlerMapping INSTANCE = new HandlerMapping(); + private static final String PATH_DELIMITER = "/"; + + private HandlerMapping() { + } + + public static HandlerMapping getInstance() { + return INSTANCE; + } + + public Handler getHandler(final HttpRequest httpRequest) { + return getHandlerByEndPoint(httpRequest); + } + + private Handler getHandlerByEndPoint(final HttpRequest httpRequest) { + final String path = httpRequest.getPath(); + + if (path.equals(PATH_DELIMITER)) { + return RootEndPointHandler.getInstance(); + } + + if (path.contains("login")) { + return LoginHandler.getInstance(); + } + + if (path.contains("register")) { + return RegisterHandler.getInstance(); + } + + return StaticResourceHandler.getInstance(); + } + + public Handler getHandlerByException(final Exception exception) { + if (exception instanceof NotFoundException) { + return NotFoundHandler.getInstance(); + } + + if (exception instanceof UnauthorizedException) { + return UnAuthorizationHandler.getInstance(); + } + + return InternalServerErrorHandler.getInstance(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/NotFoundException.java b/tomcat/src/main/java/org/apache/coyote/NotFoundException.java new file mode 100644 index 0000000000..ac525ef6fb --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/NotFoundException.java @@ -0,0 +1,7 @@ +package org.apache.coyote; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/UnauthorizedException.java b/tomcat/src/main/java/org/apache/coyote/UnauthorizedException.java new file mode 100644 index 0000000000..d051227157 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/UnauthorizedException.java @@ -0,0 +1,7 @@ +package org.apache.coyote; + +public class UnauthorizedException extends RuntimeException { + public UnauthorizedException(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/Handler.java b/tomcat/src/main/java/org/apache/coyote/handler/Handler.java new file mode 100644 index 0000000000..6bf20e85b5 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/Handler.java @@ -0,0 +1,8 @@ +package org.apache.coyote.handler; + +import org.apache.http.request.HttpRequest; + +public abstract class Handler { + // TODO: 반환타입 HttpResponse로 변경 + public abstract String handle(HttpRequest httpRequest); +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/LoginHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/LoginHandler.java new file mode 100644 index 0000000000..1bd0b60a62 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/LoginHandler.java @@ -0,0 +1,74 @@ +package org.apache.coyote.handler; + +import java.util.Optional; +import java.util.UUID; + +import org.apache.catalina.session.SessionManager; +import org.apache.http.HttpCookie; +import org.apache.http.HttpMethod; +import org.apache.catalina.session.Session; +import org.apache.http.header.StandardHttpHeader; +import org.apache.http.request.HttpRequest; +import org.apache.http.response.HttpResponseGenerator; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; + +public class LoginHandler extends Handler { + + private static final LoginHandler INSTANCE = new LoginHandler(); + private final SessionManager sessionManager = SessionManager.getInstance(); + + private LoginHandler() { + } + + public static LoginHandler getInstance() { + return INSTANCE; + } + + @Override + public String handle(final HttpRequest httpRequest) { + if (httpRequest.isSameMethod(HttpMethod.GET)) { + return processLoginGetRequest(httpRequest); + } + + if (httpRequest.isSameMethod(HttpMethod.POST)) { + return processLoginPostRequest(httpRequest); + } + + return StaticResourceHandler.getInstance().handle(new HttpRequest("GET", "/404.html", "HTTP/1.1", null, null)); + + } + + private String processLoginGetRequest(final HttpRequest httpRequest) { + HttpCookie httpCookie = httpRequest.getHttpCookie(); + if (httpCookie == null || ! sessionManager.existsById(httpCookie.getValue("JSESSIONID"))) { + return StaticResourceHandler.getInstance().handle(new HttpRequest("GET", "/login.html", "HTTP/1.1", null, null)); + } + + return HttpResponseGenerator.getFoundResponse("/index.html"); + } + + private String processLoginPostRequest(final HttpRequest httpRequest) { + final String account = httpRequest.getFormBody("account"); + final String password = httpRequest.getFormBody("password"); + + final Optional userOptional = InMemoryUserRepository.findByAccount(account); + if (userOptional.isEmpty() || !userOptional.get().checkPassword(password)) { + return StaticResourceHandler.getInstance().handle(new HttpRequest("GET", "/401.html", "HTTP/1.1", null, null)); + } + + final Session session = new Session(UUID.randomUUID().toString()); + session.setAttribute("user", userOptional.get()); + sessionManager.add(session); + return addCookie( + HttpResponseGenerator.getFoundResponse("/index.html"), + HttpCookie.of("JSESSIONID=" + session.getId())); + } + + private String addCookie(final String response, final HttpCookie cookie) { + return response + .concat("\n") + .concat(StandardHttpHeader.SET_COOKIE.getValue() + ": " + cookie.toString()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/RegisterHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/RegisterHandler.java new file mode 100644 index 0000000000..03226fddf0 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/RegisterHandler.java @@ -0,0 +1,41 @@ +package org.apache.coyote.handler; + +import org.apache.http.HttpMethod; +import org.apache.http.request.HttpRequest; +import org.apache.http.response.HttpResponseGenerator; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; + +public class RegisterHandler extends Handler { + + private static final RegisterHandler INSTANCE = new RegisterHandler(); + + private RegisterHandler() { + } + + public static RegisterHandler getInstance() { + return INSTANCE; + } + + public String handle(final HttpRequest httpRequest) { + if (httpRequest.isSameMethod(HttpMethod.GET)) { + return StaticResourceHandler.getInstance().handle(new HttpRequest("GET", "/register.html", "HTTP/1.1", null, null)); + } + + if (httpRequest.isSameMethod(HttpMethod.POST)) { + return processRegisterPostRequest(httpRequest); + } + + return StaticResourceHandler.getInstance().handle(new HttpRequest("GET", "/401.html", "HTTP/1.1", null, null)); + } + + private String processRegisterPostRequest(final HttpRequest httpRequest) { + String[] body = httpRequest.getBody().split("&"); + String account = body[0].split("=")[1]; + String email = body[1].split("=")[1]; + String password = body[2].split("=")[1]; + InMemoryUserRepository.save(new User(account, password, email)); + return HttpResponseGenerator.getFoundResponse("/index.html"); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/RootEndPointHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/RootEndPointHandler.java new file mode 100644 index 0000000000..eb090dd566 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/RootEndPointHandler.java @@ -0,0 +1,21 @@ +package org.apache.coyote.handler; + +import org.apache.http.request.HttpRequest; +import org.apache.http.response.HttpResponseGenerator; + +public class RootEndPointHandler extends Handler{ + private static final RootEndPointHandler INSTANCE = new RootEndPointHandler(); + + private RootEndPointHandler() { + } + + public static RootEndPointHandler getInstance() { + return INSTANCE; + } + + @Override + public String handle(HttpRequest httpRequest) { + final String responseBody = "Hello world!"; + return HttpResponseGenerator.getOkResponse("text/html", responseBody); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/StaticResourceHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/StaticResourceHandler.java new file mode 100644 index 0000000000..cf66652f06 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/StaticResourceHandler.java @@ -0,0 +1,65 @@ +package org.apache.coyote.handler; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import org.apache.coyote.NotFoundException; +import org.apache.http.request.HttpRequest; +import org.apache.http.response.HttpResponseGenerator; + +public class StaticResourceHandler extends Handler { + + private static final Map STATIC_RESOURCE_EXTENSIONS = Map.of( + "html", "", + "css", "/css", + "js", "/assets" + ); + private static final String STATIC_RESOURCE_ROOT_PATH = "static"; + private static final String DEFAULT_EXTENSION = ".html"; + private static final String PATH_DELIMITER = "/"; + + private static final StaticResourceHandler INSTANCE = new StaticResourceHandler(); + + private StaticResourceHandler() { + } + + public static StaticResourceHandler getInstance() { + return INSTANCE; + } + + public String handle(final HttpRequest httpRequest) { + final URL resourceURL = getClass().getClassLoader().getResource(findResourcePath(httpRequest.getPath())); + try { + final Path resourcePath = Path.of(resourceURL.getPath()); + final String responseBody = Files.readString(resourcePath); + final String mimeType = Files.probeContentType(resourcePath); + return HttpResponseGenerator.getOkResponse(mimeType, responseBody); + } catch (NullPointerException e) { + throw new NotFoundException(e.getMessage()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String findResourcePath(final String path) { + final String[] resourceNames = findResourceName(path).split("\\."); + final String extension = resourceNames[resourceNames.length - 1]; + + if (STATIC_RESOURCE_EXTENSIONS.containsKey(extension)) { + return STATIC_RESOURCE_ROOT_PATH + .concat(path); + } + + return STATIC_RESOURCE_ROOT_PATH + .concat(path) + .concat(DEFAULT_EXTENSION); + } + + private String findResourceName(final String path) { + final String[] paths = path.split(PATH_DELIMITER); + return paths[paths.length - 1]; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/exception/InternalServerErrorHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/exception/InternalServerErrorHandler.java new file mode 100644 index 0000000000..8997726f74 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/exception/InternalServerErrorHandler.java @@ -0,0 +1,22 @@ +package org.apache.coyote.handler.exception; + +import org.apache.coyote.handler.Handler; +import org.apache.http.request.HttpRequest; +import org.apache.http.response.HttpResponseGenerator; + +public class InternalServerErrorHandler extends Handler { + private static final InternalServerErrorHandler INSTANCE = new InternalServerErrorHandler(); + + private InternalServerErrorHandler() { + } + + public static InternalServerErrorHandler getInstance() { + return INSTANCE; + } + + @Override + public String handle(final HttpRequest httpRequest) { + return HttpResponseGenerator.getInternalServerErrorResponse(); + } +} + diff --git a/tomcat/src/main/java/org/apache/coyote/handler/exception/NotFoundHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/exception/NotFoundHandler.java new file mode 100644 index 0000000000..9370faf633 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/exception/NotFoundHandler.java @@ -0,0 +1,21 @@ +package org.apache.coyote.handler.exception; + +import org.apache.coyote.handler.Handler; +import org.apache.http.request.HttpRequest; +import org.apache.http.response.HttpResponseGenerator; + +public class NotFoundHandler extends Handler { + private static final NotFoundHandler INSTANCE = new NotFoundHandler(); + + private NotFoundHandler() { + } + + public static NotFoundHandler getInstance() { + return INSTANCE; + } + + @Override + public String handle(final HttpRequest httpRequest) { + return HttpResponseGenerator.getNotFountResponse(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/exception/UnAuthorizationHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/exception/UnAuthorizationHandler.java new file mode 100644 index 0000000000..061c1c9432 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/exception/UnAuthorizationHandler.java @@ -0,0 +1,21 @@ +package org.apache.coyote.handler.exception; + +import org.apache.coyote.handler.Handler; +import org.apache.http.request.HttpRequest; +import org.apache.http.response.HttpResponseGenerator; + +public class UnAuthorizationHandler extends Handler { + private static final UnAuthorizationHandler INSTANCE = new UnAuthorizationHandler(); + + private UnAuthorizationHandler() { + } + + public static UnAuthorizationHandler getInstance() { + return INSTANCE; + } + + @Override + public String handle(final HttpRequest httpRequest) { + return HttpResponseGenerator.getUnauthorizedResponse(); + } +} 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..096c62b4aa 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,12 +1,21 @@ package org.apache.coyote.http11; -import com.techcourse.exception.UncheckedServletException; +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 org.apache.coyote.handler.Handler; +import org.apache.http.request.HttpRequest; +import org.apache.http.request.HttpRequestReader; import org.apache.coyote.Processor; +import org.apache.coyote.HandlerMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.Socket; +import com.techcourse.exception.UncheckedServletException; public class Http11Processor implements Runnable, Processor { @@ -26,17 +35,12 @@ 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!"; + try (final InputStream inputStream = connection.getInputStream(); + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + final OutputStream outputStream = connection.getOutputStream()) { - final var response = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: " + responseBody.getBytes().length + " ", - "", - responseBody); + final HttpRequest httpRequest = HttpRequestReader.readHttpRequest(bufferedReader); + final String response = process(httpRequest); outputStream.write(response.getBytes()); outputStream.flush(); @@ -44,4 +48,15 @@ public void process(final Socket connection) { log.error(e.getMessage(), e); } } + + private String process(final HttpRequest httpRequest) { + final HandlerMapping handlerMapping = HandlerMapping.getInstance(); + final Handler handler = handlerMapping.getHandler(httpRequest); + + try { + return handler.handle(httpRequest); + } catch (Exception e) { + return handlerMapping.getHandlerByException(e).handle(httpRequest); + } + } } diff --git a/tomcat/src/main/java/org/apache/http/HttpCookie.java b/tomcat/src/main/java/org/apache/http/HttpCookie.java new file mode 100644 index 0000000000..783ca2fbfc --- /dev/null +++ b/tomcat/src/main/java/org/apache/http/HttpCookie.java @@ -0,0 +1,44 @@ +package org.apache.http; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class HttpCookie { + private static final String COOKIE_SEPARATOR = "; "; + private static final String KEY_VALUE_SEPARATOR = "="; + + private final Map cookie; + + public HttpCookie(Map cookie) { + this.cookie = cookie; + } + + public static HttpCookie of(String cookie) { + return new HttpCookie(parseCookie(cookie)); + } + + private static Map parseCookie(String cookie) { + String[] cookiePairs = cookie.split(COOKIE_SEPARATOR); + + Map cookies = new HashMap<>(); + for (String pair : cookiePairs) { + String[] keyAndValue = pair.split(KEY_VALUE_SEPARATOR); + cookies.put(keyAndValue[0], keyAndValue[1]); + } + + return cookies; + } + + public String getValue(String key) { + return cookie.get(key); + } + + @Override + public String toString() { + return cookie.entrySet() + .stream() + .map(entry -> entry.getKey() + KEY_VALUE_SEPARATOR + entry.getValue()) + .collect(Collectors.joining(COOKIE_SEPARATOR)); + } +} diff --git a/tomcat/src/main/java/org/apache/http/HttpMethod.java b/tomcat/src/main/java/org/apache/http/HttpMethod.java new file mode 100644 index 0000000000..6d1f5fc047 --- /dev/null +++ b/tomcat/src/main/java/org/apache/http/HttpMethod.java @@ -0,0 +1,14 @@ +package org.apache.http; + +public enum HttpMethod { + GET, + POST, + PUT, + DELETE, + HEAD, + OPTIONS, + TRACE, + CONNECT, + PATCH, + ; +} diff --git a/tomcat/src/main/java/org/apache/http/header/HttpHeader.java b/tomcat/src/main/java/org/apache/http/header/HttpHeader.java new file mode 100644 index 0000000000..b8d03c30aa --- /dev/null +++ b/tomcat/src/main/java/org/apache/http/header/HttpHeader.java @@ -0,0 +1,24 @@ +package org.apache.http.header; + +public class HttpHeader { + private final String key; + private final String value; + + public HttpHeader(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return key + ": " + value; + } +} diff --git a/tomcat/src/main/java/org/apache/http/header/StandardHttpHeader.java b/tomcat/src/main/java/org/apache/http/header/StandardHttpHeader.java new file mode 100644 index 0000000000..8bfb023192 --- /dev/null +++ b/tomcat/src/main/java/org/apache/http/header/StandardHttpHeader.java @@ -0,0 +1,30 @@ +package org.apache.http.header; + +public enum StandardHttpHeader { + ACCEPT("Accept"), + ACCEPT_RANGES("Accept-Ranges"), + AUTHORIZATION("Authorization"), + CONNECTION("Connection"), + CONTENT_LENGTH("Content-Length"), + CONTENT_TYPE("Content-Type"), + COOKIE("Cookie"), + HOST("Host"), + LOCATION("Location"), + ORIGIN("Origin"), + SET_COOKIE("Set-Cookie"), + ; + + private final String value; + + StandardHttpHeader(String value) { + this.value = value; + } + + public boolean equalsIgnoreCase(String value) { + return this.value.equalsIgnoreCase(value); + } + + public String getValue() { + return value; + } +} diff --git a/tomcat/src/main/java/org/apache/http/request/HttpRequest.java b/tomcat/src/main/java/org/apache/http/request/HttpRequest.java new file mode 100644 index 0000000000..5314a0ad4d --- /dev/null +++ b/tomcat/src/main/java/org/apache/http/request/HttpRequest.java @@ -0,0 +1,84 @@ +package org.apache.http.request; + +import java.util.Arrays; +import java.util.Optional; + +import org.apache.http.HttpCookie; +import org.apache.http.HttpMethod; +import org.apache.http.header.HttpHeader; +import org.apache.http.header.StandardHttpHeader; + +public class HttpRequest { + private final HttpMethod method; + private final String path; + private final String version; + private final HttpHeader[] headers; + private final String body; + private final HttpCookie httpCookie; + + public HttpRequest(String method, String path, String version, HttpHeader[] headers, String body) { + this.method = HttpMethod.valueOf(method); + this.path = path; + this.version = version; + this.headers = headers; + this.body = body; + this.httpCookie = parseCookie(headers); + } + + private HttpCookie parseCookie(HttpHeader[] headers) { + return Optional.ofNullable(headers) + .flatMap(hs -> Arrays.stream(hs) + .filter(header -> StandardHttpHeader.COOKIE.equalsIgnoreCase(header.getKey())) + .findFirst() + .map(header -> HttpCookie.of(header.getValue()))) + .orElse(null); + } + + public String getFormBody(String key) { + final String[] params = this.body.split("&"); + for (int i = 0; i < params.length; i++) { + String[] keyAndValue = params[i].split("="); + if (keyAndValue[0].equals(key)) { + return keyAndValue[1]; + } + } + return null; + } + + public boolean isSameMethod(HttpMethod httpMethod) { + return this.method == httpMethod; + } + + public HttpMethod getMethod() { + return method; + } + + public String getPath() { + return path; + } + + public String getVersion() { + return version; + } + + public HttpHeader[] getHeaders() { + return headers; + } + + public String getHeader(String key) { + for (HttpHeader header : headers) { + if (header.getKey().equals(key)) { + return header.getValue(); + } + } + throw new IllegalArgumentException("존재 하지 않는 Header " + key + "입니다."); + } + + public String getBody() { + return body; + } + + public HttpCookie getHttpCookie() { + return httpCookie; + } +} diff --git a/tomcat/src/main/java/org/apache/http/request/HttpRequestReader.java b/tomcat/src/main/java/org/apache/http/request/HttpRequestReader.java new file mode 100644 index 0000000000..58f0e29d0f --- /dev/null +++ b/tomcat/src/main/java/org/apache/http/request/HttpRequestReader.java @@ -0,0 +1,57 @@ +package org.apache.http.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.apache.http.header.HttpHeader; +import org.apache.http.header.StandardHttpHeader; + +public class HttpRequestReader { + + private HttpRequestReader() { + } + + public static HttpRequest readHttpRequest(final BufferedReader bufferedReader) throws IOException { + final String request = bufferedReader.readLine(); + + final String[] requestStartLine = request.split(" "); + final String method = requestStartLine[0]; + final String path = requestStartLine[1]; + final String version = requestStartLine[2]; + + final HttpHeader[] headers = readRequestHeaders(bufferedReader); + return new HttpRequest(method, path, version, headers, readRequestBody(headers, bufferedReader)); + } + + private static HttpHeader[] readRequestHeaders(final BufferedReader bufferedReader) throws IOException { + String header = bufferedReader.readLine(); + + final List headers = new ArrayList<>(); + while (header != null && !header.isEmpty()) { + String[] keyAndValue = header.split(": "); + headers.add(new HttpHeader(keyAndValue[0], keyAndValue[1])); + header = bufferedReader.readLine(); + } + + return headers.toArray(HttpHeader[]::new); + } + + private static String readRequestBody(final HttpHeader[] headers, final BufferedReader bufferedReader) throws IOException { + Optional httpHeader = Arrays.stream(headers) + .filter(header -> StandardHttpHeader.CONTENT_LENGTH.equalsIgnoreCase(header.getKey())) + .findFirst(); + + if (httpHeader.isEmpty()) { + return null; + } + + int contentLength = Integer.parseInt(httpHeader.get().getValue()); + char[] buffer = new char[contentLength]; + bufferedReader.read(buffer, 0, contentLength); + return new String(buffer); + } +} diff --git a/tomcat/src/main/java/org/apache/http/response/HttpResponseGenerator.java b/tomcat/src/main/java/org/apache/http/response/HttpResponseGenerator.java new file mode 100644 index 0000000000..fdde4fa014 --- /dev/null +++ b/tomcat/src/main/java/org/apache/http/response/HttpResponseGenerator.java @@ -0,0 +1,37 @@ +package org.apache.http.response; + +public class HttpResponseGenerator { + + private HttpResponseGenerator() { + } + + public static String getOkResponse(String mimeType, String responseBody) { + return String.join("\r\n", + "HTTP/1.1 200 OK ", + "Content-Type: " + mimeType + ";charset=utf-8 ", + "Content-Length: " + responseBody.getBytes().length + " ", + "", + responseBody); + } + + public static String getFoundResponse(String resourcePath) { + return String.join("\r\n", + "HTTP/1.1 302 Found ", + "Location: http://localhost:8080" + resourcePath); + } + + public static String getNotFountResponse() { + return String.join("\r\n", + "HTTP/1.1 404 Not Found "); + } + + public static String getUnauthorizedResponse() { + return String.join("\r\n", + "HTTP/1.1 401 Unauthorized "); + } + + public static String getInternalServerErrorResponse() { + return String.join("\r\n", + "HTTP/1.1 500 Internal Server Error "); + } +} diff --git a/tomcat/src/main/resources/static/index.html b/tomcat/src/main/resources/static/index.html index 18ac924d4e..a0255a4498 100644 --- a/tomcat/src/main/resources/static/index.html +++ b/tomcat/src/main/resources/static/index.html @@ -17,24 +17,16 @@ - - - - - - - - - - - - - - - - +
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/org/apache/catalina/SessionManagerTest.java b/tomcat/src/test/java/org/apache/catalina/SessionManagerTest.java new file mode 100644 index 0000000000..f2c97edfc7 --- /dev/null +++ b/tomcat/src/test/java/org/apache/catalina/SessionManagerTest.java @@ -0,0 +1,76 @@ +package org.apache.catalina; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionManager; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class SessionManagerTest { + + private final SessionManager sessionManager = SessionManager.getInstance(); + + @Test + @DisplayName("세션 존재 여부 확인 성공") + void existsById() { + // given + String sessionId = "1"; + Session session = new Session(sessionId); + sessionManager.add(session); + + assertThat(sessionManager.existsById(sessionId)).isTrue(); + + // after + sessionManager.remove(session); + } + + @Test + @DisplayName("세션 추가 성공") + void add() { + // given + String sessionId = "1"; + + assertThat(sessionManager.findSession(sessionId)).isNull(); + Session session = new Session(sessionId); + sessionManager.add(session); + assertThat(sessionManager.findSession(sessionId)).isEqualTo(session); + + // after + sessionManager.remove(session); + } + + @Test + @DisplayName("세션 조회 성공: 존재하지 않는 세션인 경우 null을 반환") + void findSession() { + String existSessionId = "1"; + String notExistSessionId = "2"; + + Session existSession = new Session(existSessionId); + sessionManager.add(existSession); + + assertAll( + () -> assertThat(sessionManager.findSession(existSessionId)).isEqualTo(existSession), + () -> assertThat(sessionManager.findSession(notExistSessionId)).isNull() + ); + + // after + sessionManager.remove(existSession); + } + + @Test + @DisplayName("세션 삭제 성공") + void remove() { + // given + String existSessionId = "1"; + sessionManager.add(new Session(existSessionId)); + Session savedSession = sessionManager.findSession(existSessionId); + + // when + sessionManager.remove(savedSession); + + // then + assertThat(sessionManager.findSession(savedSession.getId())).isNull(); + } +} diff --git a/tomcat/src/test/java/org/apache/catalina/session/SessionTest.java b/tomcat/src/test/java/org/apache/catalina/session/SessionTest.java new file mode 100644 index 0000000000..030766964e --- /dev/null +++ b/tomcat/src/test/java/org/apache/catalina/session/SessionTest.java @@ -0,0 +1,22 @@ +package org.apache.catalina.session; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class SessionTest { + + @Test + @DisplayName("세션의 속성값 조회") + void getAttribute() { + Session session = new Session("1"); + session.setAttribute("1", "value1"); + session.setAttribute("2", "value2"); + + assertThat(session.getAttribute("1")).isEqualTo("value1"); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/HandlerMappingTest.java b/tomcat/src/test/java/org/apache/coyote/HandlerMappingTest.java new file mode 100644 index 0000000000..04ac04e29f --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/HandlerMappingTest.java @@ -0,0 +1,79 @@ +package org.apache.coyote; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.coyote.handler.Handler; +import org.apache.coyote.handler.LoginHandler; +import org.apache.coyote.handler.RegisterHandler; +import org.apache.coyote.handler.RootEndPointHandler; +import org.apache.coyote.handler.StaticResourceHandler; +import org.apache.coyote.handler.exception.InternalServerErrorHandler; +import org.apache.coyote.handler.exception.NotFoundHandler; +import org.apache.coyote.handler.exception.UnAuthorizationHandler; +import org.apache.http.request.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class HandlerMappingTest { + + @Nested + class getHandler { + @Test + @DisplayName("루트 경로를 포함하는 경로일 경우: RootEndPointHandler 반환") + void getHandler_RootPath_ReturnsRootEndPointHandler() { + HttpRequest request = new HttpRequest("GET", "/", "HTTP/1.1", null, null); + Handler handler = HandlerMapping.getInstance().getHandler(request); + assertThat(handler).isInstanceOf(RootEndPointHandler.class); + } + + @Test + @DisplayName("login을 포함하는 경로일 경우: LoginHandler 반환") + void getHandler_LoginPath_ReturnsLoginHandler() { + HttpRequest request = new HttpRequest("GET", "/login", "HTTP/1.1", null, null); + Handler handler = HandlerMapping.getInstance().getHandler(request); + assertThat(handler).isInstanceOf(LoginHandler.class); + } + + @Test + @DisplayName("register 포함하는 경로일 경우: RegisterHandler를 반환") + void getHandler_RegisterPath_ReturnsRegisterHandler() { + HttpRequest request = new HttpRequest("GET", "/register", "HTTP/1.1", null, null); + Handler handler = HandlerMapping.getInstance().getHandler(request); + assertThat(handler).isInstanceOf(RegisterHandler.class); + } + + @Test + @DisplayName("정적 리소스 경로: StaticResourceHandler 반환") + void getHandler_StaticResourcePath_ReturnsStaticResourceHandler() { + HttpRequest request = new HttpRequest("GET", "/index.html", "HTTP/1.1", null, null); + Handler handler = HandlerMapping.getInstance().getHandler(request); + assertThat(handler).isInstanceOf(StaticResourceHandler.class); + } + } + + @Nested + class getHandlerByException { + + @Test + @DisplayName("UnauthorizedException 처리 : UnAuthorizationHandler 반환") + void getHandlerByException_UnauthorizedException() { + Handler handler = HandlerMapping.getInstance().getHandlerByException(new UnauthorizedException("권한이 없습니다.")); + assertThat(handler).isInstanceOf(UnAuthorizationHandler.class); + } + + @Test + @DisplayName("UnauthorizedException 처리 : UnAuthorizationHandler 반환") + void getHandlerByException_NotFoundException() { + Handler handler = HandlerMapping.getInstance().getHandlerByException(new NotFoundException("존재하지 않는 리소스입니다..")); + assertThat(handler).isInstanceOf(NotFoundHandler.class); + } + + @Test + @DisplayName("알 수 없는 에러: InternalServerErrornHandler 반환") + void getHandlerByException_UnknownPath_ReturnsStaticResourceHandler() { + Handler handler = HandlerMapping.getInstance().getHandlerByException(new Exception("에러")); + assertThat(handler).isInstanceOf(InternalServerErrorHandler.class); + } + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/LoginHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/handler/LoginHandlerTest.java new file mode 100644 index 0000000000..326d1ac432 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/LoginHandlerTest.java @@ -0,0 +1,97 @@ +package org.apache.coyote.handler; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; + +import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionManager; +import org.apache.http.header.HttpHeader; +import org.apache.http.request.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class LoginHandlerTest { + + @Test + @DisplayName("GET 요청 처리: 세션이 없는 경우 로그인 페이지를 반환") + void handle_GetRequest_Without_Session() throws IOException { + final URL resourceURL = getClass().getClassLoader().getResource("static/login.html"); + final String fileContent = Files.readString(Path.of(resourceURL.getPath())); + + final HttpRequest request = new HttpRequest("GET", "/login", "HTTP/1.1", null, null); + + assertTrue(LoginHandler.getInstance().handle(request).contains(fileContent)); + } + + @Test + @DisplayName("GET 요청 처리: 세션이 있는 경우 index 페이지로 리다이렉트") + void handle_GetRequest_With_ValidSession() { + final String sessionId = UUID.randomUUID().toString(); + SessionManager.getInstance().add(new Session(sessionId)); + final HttpRequest request = new HttpRequest("GET", "/login", "HTTP/1.1", + new HttpHeader[]{new HttpHeader("Cookie", "JSESSIONID=" + sessionId)}, null); + + assertTrue(LoginHandler.getInstance().handle(request).contains("302 Found")); + + SessionManager.getInstance().remove(new Session(sessionId)); + } + + @Test + @DisplayName("POST 요청 처리: 유효한 계정 정보로 로그인 성공") + void handle_PostRequest_With_ValidCredentials() { + final HttpRequest request = new HttpRequest("POST", "/login", "HTTP/1.1", null, + "account=gugu&password=password"); + + final String result = LoginHandler.getInstance().handle(request); + + assertAll( + () -> assertTrue(result.contains("302 Found")), + () -> assertTrue(result.contains("http://localhost:8080/index.html")), + () -> assertTrue(result.contains("Set-Cookie: JSESSIONID=")) + ); + assertTrue(result.contains("http://localhost:8080/index.html")); + assertTrue(result.contains("Set-Cookie: JSESSIONID=")); + } + + @Test + @DisplayName("POST 요청 처리: 비밀번호가 올바르지 않는 경우 로그인 실패") + void handle_PostRequest_With_InvalidCredentials() throws IOException { + final URL resourceURL = getClass().getClassLoader().getResource("static/401.html"); + final String fileContent = Files.readString(Path.of(resourceURL.getPath())); + + final HttpRequest request = new HttpRequest("POST", "/login", "HTTP/1.1", null, + "account=gugu&password=wrongpassword"); + + assertThat(LoginHandler.getInstance().handle(request)).contains(fileContent); + } + + @Test + @DisplayName("POST 요청 처리: 존재하지 않는 계정 정보로 로그인 실패") + void handle_PostRequest_WithNonexistentUser() throws IOException { + final URL resourceURL = getClass().getClassLoader().getResource("static/401.html"); + final String fileContent = Files.readString(Path.of(resourceURL.getPath())); + + final HttpRequest request = new HttpRequest("POST", "/login", "HTTP/1.1", null, + "account=nonexistent&password=anypassword"); + + assertThat(LoginHandler.getInstance().handle(request)).contains(fileContent); + } + + @Test + @DisplayName("지원하지 않는 메소드 처리: 404 페이지 반환") + void handle_UnsupportedMethod() throws IOException { + final URL resourceURL = getClass().getClassLoader().getResource("static/404.html"); + final String fileContent = Files.readString(Path.of(resourceURL.getPath())); + + final HttpRequest request = new HttpRequest("PUT", "/login", "HTTP/1.1", null, null); + + assertThat(LoginHandler.getInstance().handle(request)).contains(fileContent); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/RegisterHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/handler/RegisterHandlerTest.java new file mode 100644 index 0000000000..c81118e85a --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/RegisterHandlerTest.java @@ -0,0 +1,41 @@ +package org.apache.coyote.handler; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.http.request.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RegisterHandlerTest { + + @Test + @DisplayName("GET 요청 처리: 회원가입 페이지 반환") + void handle_GetRequest() throws IOException { + final URL resourceURL = getClass().getClassLoader().getResource("static/register.html"); + final String expectedResponseBody = Files.readString(Path.of(resourceURL.getPath())); + + final HttpRequest request = new HttpRequest("GET", "/register", "HTTP/1.1", null, null); + + assertThat(RegisterHandler.getInstance().handle(request)).contains(expectedResponseBody); + } + + @Test + @DisplayName("POST 요청 처리: 유효한 회원가입 정보로 회원가입 성공") + void handle_PostRequest_WithValidRegistration() { + final HttpRequest request = new HttpRequest("POST", "/register", "HTTP/1.1", null, + "account=newuser&email=newuser@example.com&password=password123"); + + final String result = RegisterHandler.getInstance().handle(request); + + assertAll( + () -> assertThat(result).contains("302 Found"), + () -> assertThat(result).contains("http://localhost:8080/index.html") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/RootEndPointHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/handler/RootEndPointHandlerTest.java new file mode 100644 index 0000000000..6577c9b511 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/RootEndPointHandlerTest.java @@ -0,0 +1,25 @@ +package org.apache.coyote.handler; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.http.request.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RootEndPointHandlerTest { + + @Test + @DisplayName("루트 엔드포인트에 대한 요청 처리: 모든 요청에 대해 동일한 응답 반환") + void handle_OtherHttpMethods() { + final String[] httpMethods = {"PUT", "DELETE", "PATCH", "OPTIONS"}; + + for (String method : httpMethods) { + final HttpRequest request = new HttpRequest(method, "/", "HTTP/1.1", null, null); + + final String result = RootEndPointHandler.getInstance().handle(request); + + assertThat(result).contains("Hello world!"); + } + } + +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/StaticResourceHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/handler/StaticResourceHandlerTest.java new file mode 100644 index 0000000000..9b5b8a0e53 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/StaticResourceHandlerTest.java @@ -0,0 +1,53 @@ +package org.apache.coyote.handler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.http.request.HttpRequest; +import org.apache.http.response.HttpResponseGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class StaticResourceHandlerTest { + + @Test + @DisplayName("정적 리소스 처리: 기본적으로 /static 경로에 있는 리소스를 반환") + void handle() throws IOException { + final URL resourceURL = getClass().getClassLoader().getResource("static/404.html"); + final String responseBody = Files.readString(Path.of(resourceURL.getPath())); + + final StaticResourceHandler handler = StaticResourceHandler.getInstance(); + final String response = handler.handle(new HttpRequest("GET", "/404.html", "HTTP/1.1", null, null)); + + assertThat(response).contains(responseBody); + } + + @Test + @DisplayName("정적 리소스 처리: 확장자에 따라 다른 경로에 있는 리소스를 반환") + void handle_When_Different_Extension_Resource() throws IOException { + final URL resourceURL = getClass().getClassLoader().getResource("static/css/styles.css"); + final String fileContent = Files.readString(Path.of(resourceURL.getPath())); + + final StaticResourceHandler handler = StaticResourceHandler.getInstance(); + final String response = handler.handle(new HttpRequest("GET", "/css/styles.css", "HTTP/1.1", null, null)); + + assertThat(response).contains(fileContent); + } + + @Test + @DisplayName("정적 리소스 처리: 확장자가 없는 경우, /static 경로에 있는 html 리소스를 반환") + void handle_When_Extension_NotExist() throws IOException { + final URL resourceURL = getClass().getClassLoader().getResource("static/404.html"); + final String responseBody = Files.readString(Path.of(resourceURL.getPath())); + + final StaticResourceHandler handler = StaticResourceHandler.getInstance(); + final String response = handler.handle(new HttpRequest("GET", "/404", "HTTP/1.1", null, null)); + + assertThat(response).contains(responseBody); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/exception/InternalServerErrorHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/handler/exception/InternalServerErrorHandlerTest.java new file mode 100644 index 0000000000..949ce7194f --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/exception/InternalServerErrorHandlerTest.java @@ -0,0 +1,18 @@ +package org.apache.coyote.handler.exception; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.http.request.HttpRequest; +import org.apache.http.response.HttpResponseGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class InternalServerErrorHandlerTest { + + @Test + @DisplayName("모든 요청 처리: 500 응답 반환") + void handle() { + HttpRequest httpRequest = new HttpRequest("GET", null, null, null, null); + assertThat(InternalServerErrorHandler.getInstance().handle(httpRequest)).isEqualTo(HttpResponseGenerator.getInternalServerErrorResponse()); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/exception/NotFoundHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/handler/exception/NotFoundHandlerTest.java new file mode 100644 index 0000000000..e1f49725f3 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/exception/NotFoundHandlerTest.java @@ -0,0 +1,19 @@ +package org.apache.coyote.handler.exception; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import org.apache.http.request.HttpRequest; +import org.apache.http.response.HttpResponseGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class NotFoundHandlerTest { + + @Test + @DisplayName("모든 요청 처리: 404 응답 반환") + void handle() { + HttpRequest httpRequest = new HttpRequest("GET", null, null, null, null); + assertThat(NotFoundHandler.getInstance().handle(httpRequest)).isEqualTo(HttpResponseGenerator.getNotFountResponse()); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/exception/UnAuthorizationHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/handler/exception/UnAuthorizationHandlerTest.java new file mode 100644 index 0000000000..0b57bd5222 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/exception/UnAuthorizationHandlerTest.java @@ -0,0 +1,18 @@ +package org.apache.coyote.handler.exception; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.http.request.HttpRequest; +import org.apache.http.response.HttpResponseGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class UnAuthorizationHandlerTest { + + @Test + @DisplayName("모든 요청 처리: 401 응답 반환") + void handle() { + HttpRequest httpRequest = new HttpRequest("GET", null, null, null, null); + assertThat(UnAuthorizationHandler.getInstance().handle(httpRequest)).isEqualTo(HttpResponseGenerator.getUnauthorizedResponse()); + } +} 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..0931626e37 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -1,18 +1,24 @@ package org.apache.coyote.http11; -import org.junit.jupiter.api.Test; -import support.StubSocket; +import static org.assertj.core.api.Assertions.assertThat; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; -import static org.assertj.core.api.Assertions.assertThat; +import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionManager; +import org.apache.http.response.HttpResponseGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import support.StubSocket; class Http11ProcessorTest { @Test + @DisplayName("루트 Get 요청 처리") void process() { // given final var socket = new StubSocket(); @@ -33,9 +39,10 @@ void process() { } @Test + @DisplayName("index.html Get 요청 처리") void index() throws IOException { // given - final String httpRequest= String.join("\r\n", + final String httpRequest = String.join("\r\n", "GET /index.html HTTP/1.1 ", "Host: localhost:8080 ", "Connection: keep-alive ", @@ -52,10 +59,133 @@ void index() throws IOException { 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"+ + "Content-Length: 5128 \r\n" + + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + @DisplayName("login Get 요청 처리: 세션이 없는 경우 login html 반환") + void login_Get_WhenExistsSession() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/login.html"); + var expected = "HTTP/1.1 200 OK \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Content-Length: 3797 \r\n" + + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + @DisplayName("login Get 요청 처리: 세션이 있는 경우 index html 리다이렉션") + void login_Get_WhenNotExistsSession() { + SessionManager sessionManager = SessionManager.getInstance(); + sessionManager.add(new Session("1")); + // given + final String httpRequest = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Cookie: JSESSIONID=1"); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + assertThat(socket.output()).isEqualTo( + HttpResponseGenerator.getFoundResponse("/index.html")); + } + + @Test + @DisplayName("login Post 요청 처리: 성공 시 index html 리다이렉트 및 Set-Cookie 헤더에 세션값 추가") + void login_Post() { + // given + final String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1", + "Host: localhost:8080", + "Content-Length: 30", + "Connection: keep-alive", + "", + "account=gugu&password=password"); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + assertThat(socket.output()).contains( + HttpResponseGenerator.getFoundResponse("/index.html"), + "Set-Cookie: JSESSIONID="); + } + + @Test + @DisplayName("register Get 요청 처리: 성공 시 register html") + void register_Get() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /register HTTP/1.1", + "Host: localhost:8080", + "Connection: keep-alive"); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/register.html"); + var expected = "HTTP/1.1 200 OK \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Content-Length: 4319 \r\n" + + "\r\n" + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); assertThat(socket.output()).isEqualTo(expected); } + + @Test + @DisplayName("register Post 요청 처리: 성공 시 index html 리다이렉트") + void register_Post() { + // given + final String httpRequest = String.join("\r\n", + "POST /register HTTP/1.1", + "Host: localhost:8080", + "Content-Length: 49", + "Connection: keep-alive", + "", + "account=gugu&email=hi@naver.com&password=password"); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + assertThat(socket.output()).contains( + HttpResponseGenerator.getFoundResponse("/index.html")); + } } diff --git a/tomcat/src/test/java/org/apache/http/HttpCookieTest.java b/tomcat/src/test/java/org/apache/http/HttpCookieTest.java new file mode 100644 index 0000000000..b3ef3e4be6 --- /dev/null +++ b/tomcat/src/test/java/org/apache/http/HttpCookieTest.java @@ -0,0 +1,34 @@ +package org.apache.http; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HttpCookieTest { + + @Test + @DisplayName("HttpCookie 생성: 문자열로부터 파싱") + void of() { + String cookieString = "name=value; session=12345"; + HttpCookie httpCookie = HttpCookie.of(cookieString); + + assertEquals("value", httpCookie.getValue("name")); + assertEquals("12345", httpCookie.getValue("session")); + } + + @Test + @DisplayName("특정 key에 대한 value 조회: 존재하지 않는 키일 경우 null 반환") + void getValue_For_NonexistentKey() { + HttpCookie httpCookie = HttpCookie.of("name=value"); + assertNull(httpCookie.getValue("nonexistent")); + } + + @Test + @DisplayName("특정 key에 대한 value 조회: 존재하지 않는 키일 경우 null 반환") + void testGetValueForNonexistentKey() { + HttpCookie httpCookie = HttpCookie.of("name=value"); + assertEquals("value", httpCookie.getValue("name")); + } +} diff --git a/tomcat/src/test/java/org/apache/http/header/StandardHttpHeaderTest.java b/tomcat/src/test/java/org/apache/http/header/StandardHttpHeaderTest.java new file mode 100644 index 0000000000..6c5e9fa15c --- /dev/null +++ b/tomcat/src/test/java/org/apache/http/header/StandardHttpHeaderTest.java @@ -0,0 +1,18 @@ +package org.apache.http.header; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class StandardHttpHeaderTest { + + @Test + void equalsIgnoreCase() { + assertAll( + () -> assertTrue(StandardHttpHeader.COOKIE.equalsIgnoreCase("cookie")), + () -> assertTrue(StandardHttpHeader.COOKIE.equalsIgnoreCase("Cookie")), + () -> assertTrue(StandardHttpHeader.COOKIE.equalsIgnoreCase("COOKIE")) + ); + } +} diff --git a/tomcat/src/test/java/org/apache/http/request/HttpRequestReaderTest.java b/tomcat/src/test/java/org/apache/http/request/HttpRequestReaderTest.java new file mode 100644 index 0000000000..11960e6c28 --- /dev/null +++ b/tomcat/src/test/java/org/apache/http/request/HttpRequestReaderTest.java @@ -0,0 +1,131 @@ +package org.apache.http.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +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 HttpRequestReaderTest { + + @Test + @DisplayName("GET 요청 읽기 테스트") + void readGetRequest() throws IOException { + // given + String rawRequest = String.join("\r\n", + "GET /index.html HTTP/1.1", + "Host: localhost:8080", + "Connection: keep-alive", + "", + ""); + BufferedReader reader = new BufferedReader(new StringReader(rawRequest)); + + // when + HttpRequest request = HttpRequestReader.readHttpRequest(reader); + + // then + assertAll( + () -> assertThat(request.getMethod().name()).isEqualTo("GET"), + () -> assertThat(request.getPath()).isEqualTo("/index.html"), + () -> assertThat(request.getVersion()).isEqualTo("HTTP/1.1"), + () -> assertThat(request.getHeaders()).hasSize(2), + () -> assertThat(request.getHeader("Host")).isEqualTo("localhost:8080"), + () -> assertThat(request.getBody()).isNull() + ); + } + + @Test + @DisplayName("POST 요청 읽기 테스트") + void readPostRequest() throws IOException { + // given + String rawRequest = String.join("\r\n", + "POST /login HTTP/1.1", + "Host: localhost:8080", + "Content-Type: application/x-www-form-urlencoded", + "Content-Length: 27", + "", + "username=john&password=pass"); + BufferedReader reader = new BufferedReader(new StringReader(rawRequest)); + + // when + HttpRequest request = HttpRequestReader.readHttpRequest(reader); + + // then + assertAll( + () -> assertThat(request.getMethod().name()).isEqualTo("POST"), + () -> assertThat(request.getPath()).isEqualTo("/login"), + () -> assertThat(request.getVersion()).isEqualTo("HTTP/1.1"), + () -> assertThat(request.getHeaders()).hasSize(3), + () -> assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-www-form-urlencoded"), + () -> assertThat(request.getBody()).isEqualTo("username=john&password=pass") + ); + } + + @Test + @DisplayName("헤더가 없는 요청 읽기 테스트") + void readRequestWithoutHeaders() throws IOException { + // given + String rawRequest = String.join("\r\n", + "GET / HTTP/1.1", + "", + ""); + BufferedReader reader = new BufferedReader(new StringReader(rawRequest)); + + // when + HttpRequest request = HttpRequestReader.readHttpRequest(reader); + + // then + assertAll( + () -> assertThat(request.getMethod().name()).isEqualTo("GET"), + () -> assertThat(request.getPath()).isEqualTo("/"), + () -> assertThat(request.getVersion()).isEqualTo("HTTP/1.1"), + () -> assertThat(request.getHeaders()).isEmpty(), + () -> assertThat(request.getBody()).isNull() + + ); + } + + @Test + @DisplayName("Content-Length가 없는 POST 요청 읽기 테스트") + void readPostRequestWithoutContentLength() throws IOException { + // given + String rawRequest = String.join("\r\n", + "POST /hi HTTP/1.1", + "Host: localhost:8080", + "Content-Type: application/x-www-form-urlencoded", + "", + ""); + BufferedReader reader = new BufferedReader(new StringReader(rawRequest)); + + // when + HttpRequest request = HttpRequestReader.readHttpRequest(reader); + + // then + assertAll( + () -> assertThat(request.getMethod().name()).isEqualTo("POST"), + () -> assertThat(request.getPath()).isEqualTo("/hi"), + () -> assertThat(request.getVersion()).isEqualTo("HTTP/1.1"), + () -> assertThat(request.getHeaders()).hasSize(2), + () -> assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-www-form-urlencoded"), + () -> assertThat(request.getBody()).isNull() + + ); + } + + @Test + @DisplayName("잘못된 형식의 요청 읽기 테스트") + void readMalformedRequest() { + // given + String rawRequest = "INVALID REQUEST"; + BufferedReader reader = new BufferedReader(new StringReader(rawRequest)); + + // when & then + assertThatThrownBy(() -> HttpRequestReader.readHttpRequest(reader)) + .isInstanceOf(ArrayIndexOutOfBoundsException.class); + } +} diff --git a/tomcat/src/test/java/org/apache/http/request/HttpRequestTest.java b/tomcat/src/test/java/org/apache/http/request/HttpRequestTest.java new file mode 100644 index 0000000000..6f00154419 --- /dev/null +++ b/tomcat/src/test/java/org/apache/http/request/HttpRequestTest.java @@ -0,0 +1,107 @@ +package org.apache.http.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.apache.http.HttpCookie; +import org.apache.http.HttpMethod; +import org.apache.http.header.HttpHeader; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HttpRequestTest { + + @Test + @DisplayName("HttpRequest 객체 생성 및 기본 getter 메소드 테스트") + void createHttpRequest() { + // given + String method = "GET"; + String path = "/index.html"; + String version = "HTTP/1.1"; + HttpHeader[] headers = new HttpHeader[]{ + new HttpHeader("Host", "localhost:8080"), + new HttpHeader("Connection", "keep-alive") + }; + String body = "sample body"; + + // when + HttpRequest request = new HttpRequest(method, path, version, headers, body); + + // then + assertAll( + () -> assertThat(request.getMethod()).isEqualTo(HttpMethod.GET), + () -> assertThat(request.getPath()).isEqualTo(path), + () -> assertThat(request.getVersion()).isEqualTo(version), + () -> assertThat(request.getHeaders()).hasSize(2), + () -> assertThat(request.getBody()).isEqualTo(body) + + ); + } + + @Test + @DisplayName("isSameMethod 메소드 테스트") + void iSameMethod() { + // given + HttpRequest request = new HttpRequest("POST", "/login", "HTTP/1.1", null, null); + + // then + assertAll( + () -> assertThat(request.isSameMethod(HttpMethod.POST)).isTrue(), + () -> assertThat(request.isSameMethod(HttpMethod.GET)).isFalse() + ); + } + + @Test + @DisplayName("getHeader 메소드 테스트: 포함되지 않은 헤더인 경우 예외") + void testGetHeader() { + // given + HttpHeader[] headers = new HttpHeader[]{ + new HttpHeader("Content-Type", "application/json"), + new HttpHeader("Authorization", "Bearer token") + }; + HttpRequest request = new HttpRequest("GET", "/api", "HTTP/1.1", headers, null); + + // when & then + assertAll( + () -> assertThat(request.getHeader("Content-Type")).isEqualTo("application/json"), + () -> assertThat(request.getHeader("Authorization")).isEqualTo("Bearer token"), + () -> assertThatThrownBy(() -> request.getHeader("Non-Existent")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("존재 하지 않는 Header") + ); + } + + @Test + @DisplayName("HttpCookie 파싱 테스트") + void testParseCookie() { + // given + HttpHeader[] headers = new HttpHeader[]{ + new HttpHeader("Cookie", "sessionId=abc123; userId=john") + }; + HttpRequest request = new HttpRequest("GET", "/", "HTTP/1.1", headers, null); + + // when + HttpCookie cookie = request.getHttpCookie(); + + // then + assertAll( + () -> assertThat(cookie).isNotNull(), + () -> assertThat(cookie.getValue("sessionId")).isEqualTo("abc123"), + () -> assertThat(cookie.getValue("userId")).isEqualTo("john") + ); + } + + @Test + @DisplayName("Cookie 헤더가 없는 경우 HttpCookie null 테스트") + void testNoCookie() { + // given + HttpHeader[] headers = new HttpHeader[]{ + new HttpHeader("Content-Type", "text/html") + }; + HttpRequest request = new HttpRequest("GET", "/", "HTTP/1.1", headers, null); + + assertThat(request.getHttpCookie()).isNull(); + } +} +