From fba6fbd742b0be4c01a5760a939aba6fbb515df1 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Sat, 27 Sep 2025 22:55:15 +0900 Subject: [PATCH 01/41] [Feat] Requirement 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 요구사항 1: index.html 반환하기 --- src/main/java/webserver/RequestHandler.java | 68 ++++++++++++++++++++- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 614a755..8956b4e 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -2,6 +2,8 @@ import java.io.*; import java.net.Socket; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.logging.Level; import java.util.logging.Logger; @@ -20,9 +22,69 @@ public void run() { BufferedReader br = new BufferedReader(new InputStreamReader(in)); DataOutputStream dos = new DataOutputStream(out); - byte[] body = "Hello World".getBytes(); - response200Header(dos, body.length); - responseBody(dos, body); + // HTTP 요청 라인 읽기 -> etc: "GET /index.html HTTP/1.1" + String requestLine = br.readLine(); + log.log(Level.INFO, "Request Line: " + requestLine); + + // 요청 라인이 비어있으면 연결 종료 + if (requestLine == null || requestLine.isEmpty()) { + return; + } + + // HTTP 요청 라인을 공백으로 분리 + // parts[0]: HTTP 메서드 (GET, POST 등) + // parts[1]: 요청 경로 (/index.html, /users/form.html +..) + // parts[2]: HTTP 버전 (HTTP/3) + String[] requestParts = requestLine.split(" "); + if (requestParts.length != 3) { + log.log(Level.WARNING, "Invalid Request Line: " + requestLine); + } + + String method = requestParts[0]; + String fullPath = requestParts[1]; + String httpVersion = requestParts[2]; + + // 쿼리 파라미터 제거 (예: /user/form.html?name=john -> /user/form.html + String path = fullPath; + if (fullPath.contains("?")) { + path = fullPath.substring(0, fullPath.indexOf("?")); + } + log.log(Level.INFO, "Method: " + method + ", Path: " + path + ", Version: " + httpVersion); + + // 경로에 따른 파일 매핑 로직 + // 1. 루트 경로 ("/") 처리 - 기본 페이지로 리다이렉트 + if (path.equals("/")) { + path = "/index.html"; + } + + // 2. 보안 검증 - ../ 과 같은 디렉토리 traversal 공격 방지 + if (path.contains("..")) { + log.log(Level.WARNING, "Path contains invalid path: " + path); + // TODO: 에러 반환 + return; + } + + // 3. webapp 폴더 기준으로 실제 파일 경로 생성 + String filePath = "webapp" + path; + log.log(Level.INFO, "File path: " + filePath); + + try { + // 4. 파일 존재 여부 확인 및 읽기 + byte[] fileContent = Files.readAllBytes(Paths.get(filePath)); + log.log(Level.INFO, "File read successfully: " + filePath); + + // 5. 성공적으로 읽었으면 200 OK 응답 + response200Header(dos, fileContent.length); + responseBody(dos, fileContent); + + } catch (IOException fileException) { + // 6. 파일이 없거나 읽기 실패시 로그 기록 + log.log(Level.WARNING, "File not found or read error: " + filePath); + // TODO: 404 에러 응답 구현 + byte[] errorBody = "404 Not Found".getBytes(); + response200Header(dos, errorBody.length); + responseBody(dos, errorBody); + } } catch (IOException e) { log.log(Level.SEVERE,e.getMessage()); From d90c6bbb967bef72d4438b4c9ae1a051e285e5ea Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Sat, 27 Sep 2025 23:20:06 +0900 Subject: [PATCH 02/41] [Feat] Add HTTP header and body parsing to RequestHandler --- src/main/java/webserver/RequestHandler.java | 26 +++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 8956b4e..a3310f7 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -7,6 +7,8 @@ import java.util.logging.Level; import java.util.logging.Logger; +import http.util.IOUtils; + public class RequestHandler implements Runnable{ Socket connection; private static final Logger log = Logger.getLogger(RequestHandler.class.getName()); @@ -19,8 +21,8 @@ public RequestHandler(Socket connection) { public void run() { log.log(Level.INFO, "New Client Connect! Connected IP : " + connection.getInetAddress() + ", Port : " + connection.getPort()); try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()){ - BufferedReader br = new BufferedReader(new InputStreamReader(in)); - DataOutputStream dos = new DataOutputStream(out); + BufferedReader br = new BufferedReader(new InputStreamReader(in)); // 라인 단위 읽기 기능 추가 + DataOutputStream dos = new DataOutputStream(out); // 바이트 단위 쓰기 기능 추가 // HTTP 요청 라인 읽기 -> etc: "GET /index.html HTTP/1.1" String requestLine = br.readLine(); @@ -51,6 +53,26 @@ public void run() { } log.log(Level.INFO, "Method: " + method + ", Path: " + path + ", Version: " + httpVersion); + // HTTP 헤더들 읽기 (빈 라인까지) + String headerLine; + int contentLength = 0; + while ((headerLine = br.readLine()) != null && !headerLine.isEmpty()) { + log.log(Level.INFO, "Header: " + headerLine); + + // Content-Length 헤더 파싱 + if (headerLine.startsWith("Content-Length:")) { + contentLength = Integer.parseInt(headerLine.substring(15).trim()); + } + } + log.log(Level.INFO, "Headers read complete. Content-Length: " + contentLength); + + // POST 요청의 바디 데이터 읽기 + String requestBody = null; + if ("POST".equals(method) && contentLength > 0) { + requestBody = IOUtils.readData(br, contentLength); + log.log(Level.INFO, "Request body: " + requestBody); + } + // 경로에 따른 파일 매핑 로직 // 1. 루트 경로 ("/") 처리 - 기본 페이지로 리다이렉트 if (path.equals("/")) { From 48235cecf296db9b60d1f031ccd932218cc15bbb Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 13:56:16 +0900 Subject: [PATCH 03/41] [Feat] Add helper for sending HTTP 302 redirect response --- src/main/java/webserver/RequestHandler.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index a3310f7..4c1e01a 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -124,6 +124,16 @@ private void response200Header(DataOutputStream dos, int lengthOfBodyContent) { } } + private void response302Header(DataOutputStream dos, String redirectPath) { + try { + dos.writeBytes("HTTP/1.1 302 Found\r\n"); + dos.writeBytes("Location: " + redirectPath + "\r\n"); + dos.writeBytes("\r\n"); + } catch (IOException e) { + log.log(Level.SEVERE, e.getMessage()); + } + } + private void responseBody(DataOutputStream dos, byte[] body) { try { dos.write(body, 0, body.length); From 148c4b30d525b9c95da7068d63cc53aea9926a0d Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 14:09:26 +0900 Subject: [PATCH 04/41] [Feat] Requirement 2 --- src/main/java/webserver/RequestHandler.java | 37 +++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 4c1e01a..2e6d701 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -6,8 +6,12 @@ import java.nio.file.Paths; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.Map; +import db.MemoryUserRepository; import http.util.IOUtils; +import http.util.HttpRequestUtils; +import model.User; public class RequestHandler implements Runnable{ Socket connection; @@ -48,9 +52,12 @@ public void run() { // 쿼리 파라미터 제거 (예: /user/form.html?name=john -> /user/form.html String path = fullPath; + String queryString = null; if (fullPath.contains("?")) { path = fullPath.substring(0, fullPath.indexOf("?")); + queryString = fullPath.substring(fullPath.indexOf("?") + 1); } + log.log(Level.INFO, "Path: " + path + ", Query String: " + queryString); log.log(Level.INFO, "Method: " + method + ", Path: " + path + ", Version: " + httpVersion); // HTTP 헤더들 읽기 (빈 라인까지) @@ -65,7 +72,7 @@ public void run() { } } log.log(Level.INFO, "Headers read complete. Content-Length: " + contentLength); - + // POST 요청의 바디 데이터 읽기 String requestBody = null; if ("POST".equals(method) && contentLength > 0) { @@ -74,6 +81,30 @@ public void run() { } // 경로에 따른 파일 매핑 로직 + // 회원 가입 처리 + if (path.equals("/user/signup") && queryString != null) { + // queryString parsing + Map params = HttpRequestUtils.parseQueryParameter(queryString); + log.log(Level.INFO, "Signup params: " + params); + + // User 객체 생성 + User newUser = new User( + params.get("userId"), + params.get("password"), + params.get("name"), + params.get("email") + ); + + // 메모리 저장소에 저장 + MemoryUserRepository repository = MemoryUserRepository.getInstance(); + repository.addUser(newUser); + log.log(Level.INFO, "New User Registered: " + newUser.getUserId()); + + // 302 리다이렉트로 메인 페이지로 이동 + response302Header(dos); + return; + } + // 1. 루트 경로 ("/") 처리 - 기본 페이지로 리다이렉트 if (path.equals("/")) { path = "/index.html"; @@ -124,10 +155,10 @@ private void response200Header(DataOutputStream dos, int lengthOfBodyContent) { } } - private void response302Header(DataOutputStream dos, String redirectPath) { + private void response302Header(DataOutputStream dos) { try { dos.writeBytes("HTTP/1.1 302 Found\r\n"); - dos.writeBytes("Location: " + redirectPath + "\r\n"); + dos.writeBytes("Location: " + "/index.html" + "\r\n"); dos.writeBytes("\r\n"); } catch (IOException e) { log.log(Level.SEVERE, e.getMessage()); From 71da5b0ba2595ac7b57645799985d4eecb322ce7 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 14:11:42 +0900 Subject: [PATCH 05/41] [Fix] Change Get -> Post --- webapp/user/form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/user/form.html b/webapp/user/form.html index 77a96ae..168cca2 100644 --- a/webapp/user/form.html +++ b/webapp/user/form.html @@ -55,7 +55,7 @@

Sign up

-
+
From 8fc5f74e39f8213263efddfdf09bcbf4529a12d0 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 14:19:13 +0900 Subject: [PATCH 06/41] [Feat] Requirement 3: POST SignUp --- src/main/java/webserver/RequestHandler.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 2e6d701..f9dbbad 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -82,10 +82,19 @@ public void run() { // 경로에 따른 파일 매핑 로직 // 회원 가입 처리 - if (path.equals("/user/signup") && queryString != null) { + if (path.equals("/user/signup") && (queryString != null || "POST".equals(method))) { // queryString parsing - Map params = HttpRequestUtils.parseQueryParameter(queryString); - log.log(Level.INFO, "Signup params: " + params); + Map params; + + if ("POST".equals(method)) { + // body에서 파라미터 추출 + params = HttpRequestUtils.parseQueryParameter(requestBody); + log.log(Level.INFO, "POST Signup params: " + params); + } else { + // GET에서 파라미터 추출 + params = HttpRequestUtils.parseQueryParameter(queryString); + log.log(Level.INFO, "GET Signup params: " + params); + } // User 객체 생성 User newUser = new User( From 6ba9cfad66cb3754ea0993a3ff10dd00b4f60277 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 14:19:57 +0900 Subject: [PATCH 07/41] [Fix] HTTP Header Handler method fix --- src/main/java/webserver/RequestHandler.java | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index f9dbbad..97ccde2 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -61,22 +61,23 @@ public void run() { log.log(Level.INFO, "Method: " + method + ", Path: " + path + ", Version: " + httpVersion); // HTTP 헤더들 읽기 (빈 라인까지) - String headerLine; - int contentLength = 0; - while ((headerLine = br.readLine()) != null && !headerLine.isEmpty()) { - log.log(Level.INFO, "Header: " + headerLine); - - // Content-Length 헤더 파싱 - if (headerLine.startsWith("Content-Length:")) { - contentLength = Integer.parseInt(headerLine.substring(15).trim()); + int requestContentLength = 0; + while (true) { + final String line = br.readLine(); + if (line.equals("")) { + break; + } + // header info + if (line.startsWith("Content-Length")) { + requestContentLength = Integer.parseInt(line.split(": ")[1]); } } - log.log(Level.INFO, "Headers read complete. Content-Length: " + contentLength); + log.log(Level.INFO, "Headers read complete. Content-Length: " + requestContentLength); // POST 요청의 바디 데이터 읽기 String requestBody = null; - if ("POST".equals(method) && contentLength > 0) { - requestBody = IOUtils.readData(br, contentLength); + if ("POST".equals(method) && requestContentLength > 0) { + requestBody = IOUtils.readData(br, requestContentLength); log.log(Level.INFO, "Request body: " + requestBody); } From 913652b04e8725244da6ec0adfe52315293b89ca Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 14:38:32 +0900 Subject: [PATCH 08/41] [Feat] Requirement 5 - Add login handling and improve redirect logic --- src/main/java/webserver/RequestHandler.java | 44 +++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 97ccde2..b7ae125 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -111,10 +111,37 @@ public void run() { log.log(Level.INFO, "New User Registered: " + newUser.getUserId()); // 302 리다이렉트로 메인 페이지로 이동 - response302Header(dos); + response302Header(dos, "/index.html"); return; } + // 로그인 처리 + if (path.equals("/user/login") && "POST".equals(method)) { + // POST 방식의 로그인만 처리 + Map params = HttpRequestUtils.parseQueryParameter(requestBody); + log.log(Level.INFO, "Login params: " + params); + + String userId = params.get("userId"); + String password = params.get("password"); + + // MemoryUserRepository에서 사용자 조회 + MemoryUserRepository repository = MemoryUserRepository.getInstance(); + User user = repository.findUserById(userId); + + // 인증 검증 + if (user != null && user.getPassword().equals(password)) { + // 로그인 성공: Cookie 설정 + 메인페이지로 리다이렉트 + log.log(Level.INFO, "Login successful: " + userId); + response302HeaderWithCookie(dos, "/index.html", "logined=true"); + return; + } else { + // 로그인 실패: 에러페이지로 리다이렉트 + log.log(Level.WARNING, "Login failed: " + userId); + response302Header(dos, "/user/login_failed.html"); + return; + } + } + // 1. 루트 경로 ("/") 처리 - 기본 페이지로 리다이렉트 if (path.equals("/")) { path = "/index.html"; @@ -165,10 +192,21 @@ private void response200Header(DataOutputStream dos, int lengthOfBodyContent) { } } - private void response302Header(DataOutputStream dos) { + private void response302Header(DataOutputStream dos, String redirectPath) { + try { + dos.writeBytes("HTTP/1.1 302 Found\r\n"); + dos.writeBytes("Location: " + redirectPath + "\r\n"); + dos.writeBytes("\r\n"); + } catch (IOException e) { + log.log(Level.SEVERE, e.getMessage()); + } + } + + private void response302HeaderWithCookie(DataOutputStream dos, String redirectPath, String cookieValue) { try { dos.writeBytes("HTTP/1.1 302 Found\r\n"); - dos.writeBytes("Location: " + "/index.html" + "\r\n"); + dos.writeBytes("Set-Cookie: " + cookieValue + "\r\n"); + dos.writeBytes("Location: " + redirectPath + "\r\n"); dos.writeBytes("\r\n"); } catch (IOException e) { log.log(Level.SEVERE, e.getMessage()); From 8e2a33e90824a64c13de41a7f58e3f9a04944c72 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 14:50:38 +0900 Subject: [PATCH 09/41] [Feat] Requirement 6 - Add login check for /user/userList route --- src/main/java/webserver/RequestHandler.java | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index b7ae125..8e21c4c 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -62,6 +62,8 @@ public void run() { // HTTP 헤더들 읽기 (빈 라인까지) int requestContentLength = 0; + String cookieValue = null; + while (true) { final String line = br.readLine(); if (line.equals("")) { @@ -71,6 +73,11 @@ public void run() { if (line.startsWith("Content-Length")) { requestContentLength = Integer.parseInt(line.split(": ")[1]); } + // Cookie parsing + if (line.startsWith("Cookie")) { + cookieValue = line.split(": ")[1]; + log.log(Level.INFO, "Cookie received: " + cookieValue); + } } log.log(Level.INFO, "Headers read complete. Content-Length: " + requestContentLength); @@ -142,6 +149,19 @@ public void run() { } } + // userList 경로 처리 + if (path.equals("/user/userList")) { + // Cookie 에서 로그인 상태 확인 + if (cookieValue != null && cookieValue.contains("logined=true")) { + // user/list.html 파일 + path = "/user/list.html"; + } else { + // 비로그인 상태 + response302Header(dos, "/user/login.html"); + return; + } + } + // 1. 루트 경로 ("/") 처리 - 기본 페이지로 리다이렉트 if (path.equals("/")) { path = "/index.html"; From 626e901e7ebddf7c0d61124d55991469844abbb3 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 14:50:57 +0900 Subject: [PATCH 10/41] [Feat] Requirement 7 - getContentType --- src/main/java/webserver/RequestHandler.java | 26 +++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 8e21c4c..150d045 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -184,7 +184,8 @@ public void run() { log.log(Level.INFO, "File read successfully: " + filePath); // 5. 성공적으로 읽었으면 200 OK 응답 - response200Header(dos, fileContent.length); + String contentType = getContentType(filePath); + response200Header(dos, fileContent.length, contentType); responseBody(dos, fileContent); } catch (IOException fileException) { @@ -192,7 +193,8 @@ public void run() { log.log(Level.WARNING, "File not found or read error: " + filePath); // TODO: 404 에러 응답 구현 byte[] errorBody = "404 Not Found".getBytes(); - response200Header(dos, errorBody.length); + + response200Header(dos, errorBody.length, "text/html;charset=utf-8"); responseBody(dos, errorBody); } @@ -201,10 +203,10 @@ public void run() { } } - private void response200Header(DataOutputStream dos, int lengthOfBodyContent) { + private void response200Header(DataOutputStream dos, int lengthOfBodyContent, String contentType) { try { dos.writeBytes("HTTP/1.1 200 OK \r\n"); - dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n"); + dos.writeBytes("Content-Type: " + contentType + "\r\n"); // 동적 설정 dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n"); dos.writeBytes("\r\n"); } catch (IOException e) { @@ -233,6 +235,22 @@ private void response302HeaderWithCookie(DataOutputStream dos, String redirectPa } } + private String getContentType(String filePath) { + if (filePath.endsWith(".css")) { + return "text/css"; + } else if (filePath.endsWith(".js")) { + return "application/javascript"; + } else if (filePath.endsWith(".png")) { + return "image/png"; + } else if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) { + return "image/jpeg"; + } else if (filePath.endsWith(".html")) { + return "text/html;charset=utf-8"; + } else { + return "text/html;charset=utf-8"; // default + } + } + private void responseBody(DataOutputStream dos, byte[] body) { try { dos.write(body, 0, body.length); From 6e06134eddc52a19b679ea9fc431b89344dc33b6 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 14:58:26 +0900 Subject: [PATCH 11/41] [Feat] Assignment2 - Requirement 1: Make Enums --- src/main/java/http/enums/ContentType.java | 33 +++++++++++++++++++++++ src/main/java/http/enums/HttpHeader.java | 30 +++++++++++++++++++++ src/main/java/http/enums/HttpMethod.java | 26 ++++++++++++++++++ src/main/java/http/enums/HttpStatus.java | 23 ++++++++++++++++ src/main/java/http/enums/RequestPath.java | 31 +++++++++++++++++++++ src/main/java/model/UserField.java | 18 +++++++++++++ 6 files changed, 161 insertions(+) create mode 100644 src/main/java/http/enums/ContentType.java create mode 100644 src/main/java/http/enums/HttpHeader.java create mode 100644 src/main/java/http/enums/HttpMethod.java create mode 100644 src/main/java/http/enums/HttpStatus.java create mode 100644 src/main/java/http/enums/RequestPath.java create mode 100644 src/main/java/model/UserField.java diff --git a/src/main/java/http/enums/ContentType.java b/src/main/java/http/enums/ContentType.java new file mode 100644 index 0000000..c867c59 --- /dev/null +++ b/src/main/java/http/enums/ContentType.java @@ -0,0 +1,33 @@ +package http.enums; + +public enum ContentType { + TEXT_HTML("text/html;charset=utf-8"), + TEXT_CSS("text/css"), + APPLICATION_JAVASCRIPT("application/javascript"), + IMAGE_PNG("image/png"), + IMAGE_JPEG("image/jpeg"); + + private final String mimeType; + + ContentType(String mimeType) { + this.mimeType = mimeType; + } + + public String getValue() { + return mimeType; + } + + public static ContentType fromFileExtension(String filePath) { + if (filePath.endsWith(".css")) { + return TEXT_CSS; + } else if (filePath.endsWith(".js")) { + return APPLICATION_JAVASCRIPT; + } else if (filePath.endsWith(".png")) { + return IMAGE_PNG; + } else if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) { + return IMAGE_JPEG; + } else { + return TEXT_HTML; + } + } +} \ No newline at end of file diff --git a/src/main/java/http/enums/HttpHeader.java b/src/main/java/http/enums/HttpHeader.java new file mode 100644 index 0000000..9832eb9 --- /dev/null +++ b/src/main/java/http/enums/HttpHeader.java @@ -0,0 +1,30 @@ +package http.enums; + +public enum HttpHeader { + CONTENT_LENGTH("Content-Length"), + CONTENT_TYPE("Content-Type"), + COOKIE("Cookie"), + SET_COOKIE("Set-Cookie"), + LOCATION("Location"), + HOST("Host"), + USER_AGENT("User-Agent"); + + private final String headerName; + + HttpHeader(String headerName) { + this.headerName = headerName; + } + + public String getValue() { + return headerName; + } + + public static HttpHeader from(String headerName) { + for (HttpHeader header : values()) { + if (header.headerName.equals(headerName)) { + return header; + } + } + return null; // 알려지지 않은 헤더는 null return + } +} \ No newline at end of file diff --git a/src/main/java/http/enums/HttpMethod.java b/src/main/java/http/enums/HttpMethod.java new file mode 100644 index 0000000..35b780b --- /dev/null +++ b/src/main/java/http/enums/HttpMethod.java @@ -0,0 +1,26 @@ +package http.enums; + +public enum HttpMethod { + GET("GET"), + POST("POST"); + + private final String method; + + HttpMethod(String method) { + this.method = method; + } + + public String getValue() { + return method; + } + + public static HttpMethod from(String method) { + for (HttpMethod httpMethod : HttpMethod.values()) { + if (httpMethod.getValue().equals(method)) { + return httpMethod; + } + } + // todo: Custom Exception + throw new IllegalArgumentException("Unknown HTTP method: " + method); + } +} \ No newline at end of file diff --git a/src/main/java/http/enums/HttpStatus.java b/src/main/java/http/enums/HttpStatus.java new file mode 100644 index 0000000..83424ea --- /dev/null +++ b/src/main/java/http/enums/HttpStatus.java @@ -0,0 +1,23 @@ +package http.enums; + +public enum HttpStatus { + OK(200, "HTTP/1.1 200 OK"), + FOUND(302, "HTTP/1.1 302 Found"), + NOT_FOUND(404, "HTTP/1.1 404 Not Found"); + + private final int code; + private final String statusLine; + + HttpStatus(int code, String statusLine) { + this.code = code; + this.statusLine = statusLine; + } + + public int getCode() { + return code; + } + + public String getStatusLine() { + return statusLine; + } +} \ No newline at end of file diff --git a/src/main/java/http/enums/RequestPath.java b/src/main/java/http/enums/RequestPath.java new file mode 100644 index 0000000..ecc0ce3 --- /dev/null +++ b/src/main/java/http/enums/RequestPath.java @@ -0,0 +1,31 @@ +package http.enums; + +public enum RequestPath { + ROOT("/"), + INDEX("/index.html"), + USER_SIGNUP("/user/signup"), + USER_LOGIN("/user/login"), + USER_LIST("/user/userList"), + USER_LIST_HTML("/user/list.html"), + USER_LOGIN_HTML("/user/login.html"), + USER_LOGIN_FAILED("/user/login_failed.html"); + + private final String path; + + RequestPath(String path) { + this.path = path; + } + + public String getValue() { + return path; + } + + public static RequestPath from(String path) { + for (RequestPath requestPath : values()) { + if (requestPath.path.equals(path)) { + return requestPath; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/model/UserField.java b/src/main/java/model/UserField.java new file mode 100644 index 0000000..cacb3b0 --- /dev/null +++ b/src/main/java/model/UserField.java @@ -0,0 +1,18 @@ +package model; + +public enum UserField { + USER_ID("userId"), + PASSWORD("password"), + NAME("name"), + EMAIL("email"); + + private final String fieldName; + + UserField(String fieldName) { + this.fieldName = fieldName; + } + + public String getValue() { + return fieldName; + } +} \ No newline at end of file From 0a5ca5b954d4801410783da75f466b522846c9e2 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 14:59:37 +0900 Subject: [PATCH 12/41] [Feat] Assignment2 - Requirement 1: Using Enums in RequestHandler --- src/main/java/webserver/RequestHandler.java | 85 ++++++++++----------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 150d045..682bb5d 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -4,14 +4,20 @@ import java.net.Socket; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.Map; import db.MemoryUserRepository; import http.util.IOUtils; import http.util.HttpRequestUtils; import model.User; +import http.enums.HttpMethod; +import http.enums.HttpHeader; +import http.enums.HttpStatus; +import http.enums.ContentType; +import http.enums.RequestPath; +import model.UserField; public class RequestHandler implements Runnable{ Socket connection; @@ -70,11 +76,11 @@ public void run() { break; } // header info - if (line.startsWith("Content-Length")) { + if (line.startsWith(HttpHeader.CONTENT_LENGTH.getValue())) { requestContentLength = Integer.parseInt(line.split(": ")[1]); } // Cookie parsing - if (line.startsWith("Cookie")) { + if (line.startsWith(HttpHeader.COOKIE.getValue())) { cookieValue = line.split(": ")[1]; log.log(Level.INFO, "Cookie received: " + cookieValue); } @@ -83,18 +89,18 @@ public void run() { // POST 요청의 바디 데이터 읽기 String requestBody = null; - if ("POST".equals(method) && requestContentLength > 0) { + if (HttpMethod.POST.getValue().equals(method) && requestContentLength > 0) { requestBody = IOUtils.readData(br, requestContentLength); log.log(Level.INFO, "Request body: " + requestBody); } // 경로에 따른 파일 매핑 로직 // 회원 가입 처리 - if (path.equals("/user/signup") && (queryString != null || "POST".equals(method))) { + if (path.equals(RequestPath.USER_SIGNUP.getValue()) && (queryString != null || HttpMethod.POST.getValue().equals(method))) { // queryString parsing Map params; - if ("POST".equals(method)) { + if (HttpMethod.POST.getValue().equals(method)) { // body에서 파라미터 추출 params = HttpRequestUtils.parseQueryParameter(requestBody); log.log(Level.INFO, "POST Signup params: " + params); @@ -106,10 +112,10 @@ public void run() { // User 객체 생성 User newUser = new User( - params.get("userId"), - params.get("password"), - params.get("name"), - params.get("email") + params.get(UserField.USER_ID.getValue()), + params.get(UserField.PASSWORD.getValue()), + params.get(UserField.NAME.getValue()), + params.get(UserField.EMAIL.getValue()) ); // 메모리 저장소에 저장 @@ -118,18 +124,18 @@ public void run() { log.log(Level.INFO, "New User Registered: " + newUser.getUserId()); // 302 리다이렉트로 메인 페이지로 이동 - response302Header(dos, "/index.html"); + response302Header(dos, RequestPath.INDEX.getValue()); return; } // 로그인 처리 - if (path.equals("/user/login") && "POST".equals(method)) { + if (path.equals(RequestPath.USER_LOGIN.getValue()) && HttpMethod.POST.getValue().equals(method)) { // POST 방식의 로그인만 처리 Map params = HttpRequestUtils.parseQueryParameter(requestBody); log.log(Level.INFO, "Login params: " + params); - String userId = params.get("userId"); - String password = params.get("password"); + String userId = params.get(UserField.USER_ID.getValue()); + String password = params.get(UserField.PASSWORD.getValue()); // MemoryUserRepository에서 사용자 조회 MemoryUserRepository repository = MemoryUserRepository.getInstance(); @@ -139,32 +145,32 @@ public void run() { if (user != null && user.getPassword().equals(password)) { // 로그인 성공: Cookie 설정 + 메인페이지로 리다이렉트 log.log(Level.INFO, "Login successful: " + userId); - response302HeaderWithCookie(dos, "/index.html", "logined=true"); + response302HeaderWithCookie(dos, RequestPath.INDEX.getValue(), "logined=true"); return; } else { // 로그인 실패: 에러페이지로 리다이렉트 log.log(Level.WARNING, "Login failed: " + userId); - response302Header(dos, "/user/login_failed.html"); + response302Header(dos, RequestPath.USER_LOGIN_FAILED.getValue()); return; } } // userList 경로 처리 - if (path.equals("/user/userList")) { + if (path.equals(RequestPath.USER_LIST.getValue())) { // Cookie 에서 로그인 상태 확인 if (cookieValue != null && cookieValue.contains("logined=true")) { // user/list.html 파일 - path = "/user/list.html"; + path = RequestPath.USER_LIST_HTML.getValue(); } else { // 비로그인 상태 - response302Header(dos, "/user/login.html"); + response302Header(dos, RequestPath.USER_LOGIN_HTML.getValue()); return; } } // 1. 루트 경로 ("/") 처리 - 기본 페이지로 리다이렉트 - if (path.equals("/")) { - path = "/index.html"; + if (path.equals(RequestPath.ROOT.getValue())) { + path = RequestPath.INDEX.getValue(); } // 2. 보안 검증 - ../ 과 같은 디렉토리 traversal 공격 방지 @@ -182,19 +188,19 @@ public void run() { // 4. 파일 존재 여부 확인 및 읽기 byte[] fileContent = Files.readAllBytes(Paths.get(filePath)); log.log(Level.INFO, "File read successfully: " + filePath); - + // 5. 성공적으로 읽었으면 200 OK 응답 String contentType = getContentType(filePath); response200Header(dos, fileContent.length, contentType); responseBody(dos, fileContent); - + } catch (IOException fileException) { // 6. 파일이 없거나 읽기 실패시 로그 기록 log.log(Level.WARNING, "File not found or read error: " + filePath); // TODO: 404 에러 응답 구현 byte[] errorBody = "404 Not Found".getBytes(); - response200Header(dos, errorBody.length, "text/html;charset=utf-8"); + response200Header(dos, errorBody.length, ContentType.TEXT_HTML.getValue()); responseBody(dos, errorBody); } @@ -205,9 +211,9 @@ public void run() { private void response200Header(DataOutputStream dos, int lengthOfBodyContent, String contentType) { try { - dos.writeBytes("HTTP/1.1 200 OK \r\n"); - dos.writeBytes("Content-Type: " + contentType + "\r\n"); // 동적 설정 - dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n"); + dos.writeBytes(HttpStatus.OK.getStatusLine() + " \r\n"); + dos.writeBytes(HttpHeader.CONTENT_TYPE.getValue() + ": " + contentType + "\r\n"); + dos.writeBytes(HttpHeader.CONTENT_LENGTH.getValue() + ": " + lengthOfBodyContent + "\r\n"); dos.writeBytes("\r\n"); } catch (IOException e) { log.log(Level.SEVERE, e.getMessage()); @@ -216,8 +222,8 @@ private void response200Header(DataOutputStream dos, int lengthOfBodyContent, St private void response302Header(DataOutputStream dos, String redirectPath) { try { - dos.writeBytes("HTTP/1.1 302 Found\r\n"); - dos.writeBytes("Location: " + redirectPath + "\r\n"); + dos.writeBytes(HttpStatus.FOUND.getStatusLine() + "\r\n"); + dos.writeBytes(HttpHeader.LOCATION.getValue() + ": " + redirectPath + "\r\n"); dos.writeBytes("\r\n"); } catch (IOException e) { log.log(Level.SEVERE, e.getMessage()); @@ -226,9 +232,9 @@ private void response302Header(DataOutputStream dos, String redirectPath) { private void response302HeaderWithCookie(DataOutputStream dos, String redirectPath, String cookieValue) { try { - dos.writeBytes("HTTP/1.1 302 Found\r\n"); - dos.writeBytes("Set-Cookie: " + cookieValue + "\r\n"); - dos.writeBytes("Location: " + redirectPath + "\r\n"); + dos.writeBytes(HttpStatus.FOUND.getStatusLine() + "\r\n"); + dos.writeBytes(HttpHeader.SET_COOKIE.getValue() + ": " + cookieValue + "\r\n"); + dos.writeBytes(HttpHeader.LOCATION.getValue() + ": " + redirectPath + "\r\n"); dos.writeBytes("\r\n"); } catch (IOException e) { log.log(Level.SEVERE, e.getMessage()); @@ -236,19 +242,7 @@ private void response302HeaderWithCookie(DataOutputStream dos, String redirectPa } private String getContentType(String filePath) { - if (filePath.endsWith(".css")) { - return "text/css"; - } else if (filePath.endsWith(".js")) { - return "application/javascript"; - } else if (filePath.endsWith(".png")) { - return "image/png"; - } else if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) { - return "image/jpeg"; - } else if (filePath.endsWith(".html")) { - return "text/html;charset=utf-8"; - } else { - return "text/html;charset=utf-8"; // default - } + return ContentType.fromFileExtension(filePath).getValue(); } private void responseBody(DataOutputStream dos, byte[] body) { @@ -259,5 +253,4 @@ private void responseBody(DataOutputStream dos, byte[] body) { log.log(Level.SEVERE, e.getMessage()); } } - } \ No newline at end of file From 1db727ed3c8f15b07e541fd36a1e245d165e2eea Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 16:16:25 +0900 Subject: [PATCH 13/41] [Feat] Assignment2 Requirement 2: Add HTTP request parsing classes --- src/main/java/http/HttpHeaders.java | 61 ++++++++++++++++++++ src/main/java/http/HttpRequest.java | 80 +++++++++++++++++++++++++++ src/main/java/http/HttpStartLine.java | 58 +++++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 src/main/java/http/HttpHeaders.java create mode 100644 src/main/java/http/HttpRequest.java create mode 100644 src/main/java/http/HttpStartLine.java diff --git a/src/main/java/http/HttpHeaders.java b/src/main/java/http/HttpHeaders.java new file mode 100644 index 0000000..dc6983a --- /dev/null +++ b/src/main/java/http/HttpHeaders.java @@ -0,0 +1,61 @@ +package http; + +import http.enums.HttpHeader; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class HttpHeaders { + private final Map headers; + + public HttpHeaders(Map headers) { + this.headers = new HashMap<>(headers); + } + + public static HttpHeaders from(BufferedReader br) throws IOException { + Map headerMap = new HashMap<>(); + + String line; + while ((line = br.readLine()) != null && !line.isEmpty()) { + int colonIndex = line.indexOf(":"); + if (colonIndex != -1) { + String key = line.substring(0, colonIndex).trim(); + String value = line.substring(colonIndex + 1).trim(); + headerMap.put(key, value); + } + } + + return new HttpHeaders(headerMap); + } + + public String getHeader(HttpHeader header) { + return headers.get(header.getValue()); + } + + public String getHeader(String headerName) { + return headers.get(headerName); + } + + public int getContentLength() { + String contentLength = getHeader(HttpHeader.CONTENT_LENGTH); + return contentLength != null ? Integer.parseInt(contentLength) : 0; + } + + public String getCookie() { + return getHeader(HttpHeader.COOKIE); + } + + public boolean hasHeader(HttpHeader header) { + return headers.containsKey(header.getValue()); + } + + public boolean hasHeader(String headerName) { + return headers.containsKey(headerName); + } + + public Map getAllHeaders() { + return new HashMap<>(headers); + } +} \ No newline at end of file diff --git a/src/main/java/http/HttpRequest.java b/src/main/java/http/HttpRequest.java new file mode 100644 index 0000000..d8fc54a --- /dev/null +++ b/src/main/java/http/HttpRequest.java @@ -0,0 +1,80 @@ +package http; + +import http.enums.HttpMethod; +import http.util.IOUtils; + +import java.io.BufferedReader; +import java.io.IOException; + +public class HttpRequest { + private final HttpStartLine startLine; + private final HttpHeaders headers; + private final String body; + + private HttpRequest(HttpStartLine startLine, HttpHeaders headers, String body) { + this.startLine = startLine; + this.headers = headers; + this.body = body; + } + + public static HttpRequest from(BufferedReader br) throws IOException { + String requestLine = br.readLine(); + if (requestLine == null || requestLine.isEmpty()) { + throw new IllegalArgumentException("Request line cannot be null or empty"); + } + + HttpStartLine startLine = HttpStartLine.from(requestLine); + HttpHeaders headers = HttpHeaders.from(br); + + String body = null; + if (startLine.getMethod() == HttpMethod.POST && headers.getContentLength() > 0) { + body = IOUtils.readData(br, headers.getContentLength()); + } + + return new HttpRequest(startLine, headers, body); + } + + public HttpMethod getMethod() { + return startLine.getMethod(); + } + + public String getUrl() { + return startLine.getPathWithoutQuery(); + } + + public String getPath() { + return startLine.getPathWithoutQuery(); + } + + public String getQueryString() { + return startLine.getQueryString(); + } + + public String getVersion() { + return startLine.getVersion(); + } + + public String getHeader(String headerName) { + return headers.getHeader(headerName); + } + + public String getCookie() { + return headers.getCookie(); + } + + public int getContentLength() { + return headers.getContentLength(); + } + + public String getBody() { + return body; + } + + public HttpStartLine getStartLine() { + return startLine; + } + + public HttpHeaders getHeaders() { + return headers; + } +} \ No newline at end of file diff --git a/src/main/java/http/HttpStartLine.java b/src/main/java/http/HttpStartLine.java new file mode 100644 index 0000000..e2ae3e3 --- /dev/null +++ b/src/main/java/http/HttpStartLine.java @@ -0,0 +1,58 @@ +package http; + +import http.enums.HttpMethod; + +public class HttpStartLine { + private final HttpMethod method; + private final String path; + private final String version; + + public HttpStartLine(HttpMethod method, String path, String version) { + this.method = method; + this.path = path; + this.version = version; + } + + public static HttpStartLine from(String requestLine) { + if (requestLine == null || requestLine.isEmpty()) { + throw new IllegalArgumentException("Request line cannot be null or empty"); + } + + String[] parts = requestLine.split(" "); + if (parts.length != 3) { + throw new IllegalArgumentException("Invalid request line format: " + requestLine); + } + + HttpMethod method = HttpMethod.from(parts[0]); + String path = parts[1]; + String version = parts[2]; + + return new HttpStartLine(method, path, version); + } + + public HttpMethod getMethod() { + return method; + } + + public String getPath() { + return path; + } + + public String getVersion() { + return version; + } + + public String getPathWithoutQuery() { + if (path.contains("?")) { + return path.substring(0, path.indexOf("?")); + } + return path; + } + + public String getQueryString() { + if (path.contains("?")) { + return path.substring(path.indexOf("?") + 1); + } + return null; + } +} \ No newline at end of file From d3b3d1529f381928e85015b695d63592d247cf74 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 16:16:44 +0900 Subject: [PATCH 14/41] [Feat] Assignment2 Requirement 2: Add HttpRequest parsing unit tests --- src/test/java/HttpRequestTest.java | 79 +++++++++++++++++++++++++++++ src/test/resources/get_request.txt | 5 ++ src/test/resources/post_request.txt | 7 +++ 3 files changed, 91 insertions(+) create mode 100644 src/test/java/HttpRequestTest.java create mode 100644 src/test/resources/get_request.txt create mode 100644 src/test/resources/post_request.txt diff --git a/src/test/java/HttpRequestTest.java b/src/test/java/HttpRequestTest.java new file mode 100644 index 0000000..1b7b5c5 --- /dev/null +++ b/src/test/java/HttpRequestTest.java @@ -0,0 +1,79 @@ +import http.HttpRequest; +import http.enums.HttpMethod; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.*; + +public class HttpRequestTest { + + private static final String TEST_DIRECTORY = "src/test/resources/"; + + private BufferedReader bufferedReaderFromFile(String path) throws IOException { + return new BufferedReader(new InputStreamReader(Files.newInputStream(Paths.get(path)))); + } + + @Test + @DisplayName("POST 요청을 파싱할 때 메서드, URL, 헤더, 바디가 올바르게 파싱되어야 한다") + public void parsePostRequestWithBodyAndHeaders() throws IOException { + // given + BufferedReader br = bufferedReaderFromFile(TEST_DIRECTORY + "post_request.txt"); + + // when + HttpRequest httpRequest = HttpRequest.from(br); + + // then + assertEquals(HttpMethod.POST, httpRequest.getMethod()); + assertEquals("/user/create", httpRequest.getUrl()); + assertEquals("HTTP/1.1", httpRequest.getVersion()); + assertEquals("localhost:8080", httpRequest.getHeader("Host")); + assertEquals(40, httpRequest.getContentLength()); + assertNotNull(httpRequest.getBody()); + assertTrue(httpRequest.getBody().contains("userId")); + } + + @Test + @DisplayName("GET 요청을 파싱할 때 쿼리 파라미터와 쿠키가 올바르게 파싱되어야 한다") + public void parseGetRequestWithQueryParametersAndCookies() throws IOException { + // given + BufferedReader br = bufferedReaderFromFile(TEST_DIRECTORY + "get_request.txt"); + + // when + HttpRequest httpRequest = HttpRequest.from(br); + + // then + assertEquals(HttpMethod.GET, httpRequest.getMethod()); + assertEquals("/index.html", httpRequest.getUrl()); + assertEquals("name=foden&age=26", httpRequest.getQueryString()); + assertEquals("HTTP/1.1", httpRequest.getVersion()); + assertEquals("localhost:8080", httpRequest.getHeader("Host")); + assertEquals("logined=true", httpRequest.getCookie()); + assertNull(httpRequest.getBody()); + } + + @Test + @DisplayName("잘못된 형식의 요청 라인이 주어질 때 예외가 발생해야 한다") + public void throwExceptionWhenInvalidRequestLineFormat() { + // given + BufferedReader br = new BufferedReader(new java.io.StringReader("INVALID REQUEST")); + + // when & then + assertThrows(IllegalArgumentException.class, () -> HttpRequest.from(br)); + } + + @Test + @DisplayName("빈 요청 라인이 주어질 때 예외가 발생해야 한다") + public void throwExceptionWhenEmptyRequestLine() { + // given + BufferedReader br = new BufferedReader(new java.io.StringReader("")); + + // when & then + assertThrows(IllegalArgumentException.class, () -> HttpRequest.from(br)); + } +} \ No newline at end of file diff --git a/src/test/resources/get_request.txt b/src/test/resources/get_request.txt new file mode 100644 index 0000000..84a5224 --- /dev/null +++ b/src/test/resources/get_request.txt @@ -0,0 +1,5 @@ +GET /index.html?name=foden&age=26 HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +Accept: text/html,application/xhtml+xml +Cookie: logined=true diff --git a/src/test/resources/post_request.txt b/src/test/resources/post_request.txt new file mode 100644 index 0000000..4ed4004 --- /dev/null +++ b/src/test/resources/post_request.txt @@ -0,0 +1,7 @@ +POST /user/create HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +Content-Length: 40 +Accept: */* + +userId=foden&password=0801&name=PSG \ No newline at end of file From 6a62fc7cc7dd4015478d7f3e20c04d1af938c420 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 16:17:20 +0900 Subject: [PATCH 15/41] [Feat] Assignment2 Requirement 2: Refactor RequestHandler --- src/main/java/webserver/RequestHandler.java | 78 +++++---------------- 1 file changed, 18 insertions(+), 60 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 682bb5d..1908bd1 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -9,8 +9,8 @@ import java.util.logging.Logger; import db.MemoryUserRepository; -import http.util.IOUtils; import http.util.HttpRequestUtils; +import http.HttpRequest; import model.User; import http.enums.HttpMethod; import http.enums.HttpHeader; @@ -31,66 +31,24 @@ public RequestHandler(Socket connection) { public void run() { log.log(Level.INFO, "New Client Connect! Connected IP : " + connection.getInetAddress() + ", Port : " + connection.getPort()); try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()){ - BufferedReader br = new BufferedReader(new InputStreamReader(in)); // 라인 단위 읽기 기능 추가 - DataOutputStream dos = new DataOutputStream(out); // 바이트 단위 쓰기 기능 추가 - - // HTTP 요청 라인 읽기 -> etc: "GET /index.html HTTP/1.1" - String requestLine = br.readLine(); - log.log(Level.INFO, "Request Line: " + requestLine); - - // 요청 라인이 비어있으면 연결 종료 - if (requestLine == null || requestLine.isEmpty()) { - return; - } - - // HTTP 요청 라인을 공백으로 분리 - // parts[0]: HTTP 메서드 (GET, POST 등) - // parts[1]: 요청 경로 (/index.html, /users/form.html +..) - // parts[2]: HTTP 버전 (HTTP/3) - String[] requestParts = requestLine.split(" "); - if (requestParts.length != 3) { - log.log(Level.WARNING, "Invalid Request Line: " + requestLine); - } - - String method = requestParts[0]; - String fullPath = requestParts[1]; - String httpVersion = requestParts[2]; - - // 쿼리 파라미터 제거 (예: /user/form.html?name=john -> /user/form.html - String path = fullPath; - String queryString = null; - if (fullPath.contains("?")) { - path = fullPath.substring(0, fullPath.indexOf("?")); - queryString = fullPath.substring(fullPath.indexOf("?") + 1); + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + DataOutputStream dos = new DataOutputStream(out); + + // HttpRequest 객체로 HTTP 요청 파싱 + HttpRequest httpRequest = HttpRequest.from(br); + log.log(Level.INFO, "Request Line: " + httpRequest.getMethod() + " " + httpRequest.getPath() + " " + httpRequest.getVersion()); + + String method = httpRequest.getMethod().getValue(); + String path = httpRequest.getPath(); + String queryString = httpRequest.getQueryString(); + String cookieValue = httpRequest.getCookie(); + String requestBody = httpRequest.getBody(); + + log.log(Level.INFO, "Method: " + method + ", Path: " + path + ", Query String: " + queryString); + if (cookieValue != null) { + log.log(Level.INFO, "Cookie received: " + cookieValue); } - log.log(Level.INFO, "Path: " + path + ", Query String: " + queryString); - log.log(Level.INFO, "Method: " + method + ", Path: " + path + ", Version: " + httpVersion); - - // HTTP 헤더들 읽기 (빈 라인까지) - int requestContentLength = 0; - String cookieValue = null; - - while (true) { - final String line = br.readLine(); - if (line.equals("")) { - break; - } - // header info - if (line.startsWith(HttpHeader.CONTENT_LENGTH.getValue())) { - requestContentLength = Integer.parseInt(line.split(": ")[1]); - } - // Cookie parsing - if (line.startsWith(HttpHeader.COOKIE.getValue())) { - cookieValue = line.split(": ")[1]; - log.log(Level.INFO, "Cookie received: " + cookieValue); - } - } - log.log(Level.INFO, "Headers read complete. Content-Length: " + requestContentLength); - - // POST 요청의 바디 데이터 읽기 - String requestBody = null; - if (HttpMethod.POST.getValue().equals(method) && requestContentLength > 0) { - requestBody = IOUtils.readData(br, requestContentLength); + if (requestBody != null) { log.log(Level.INFO, "Request body: " + requestBody); } From c0b73a02ea0ffc43ec87946bf9b145aea1b4a911 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:19:33 +0900 Subject: [PATCH 16/41] [Feat] Assignment2 Requirement 3: Add HttpResponse class --- src/main/java/http/HttpResponse.java | 78 ++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/main/java/http/HttpResponse.java diff --git a/src/main/java/http/HttpResponse.java b/src/main/java/http/HttpResponse.java new file mode 100644 index 0000000..0387417 --- /dev/null +++ b/src/main/java/http/HttpResponse.java @@ -0,0 +1,78 @@ +package http; + +import http.enums.ContentType; +import http.enums.HttpHeader; +import http.enums.HttpStatus; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class HttpResponse { + private final DataOutputStream dos; + private final String webappPath; + + public HttpResponse(OutputStream outputStream) { + this(outputStream, "webapp"); + } + + public HttpResponse(OutputStream outputStream, String webappPath) { + this.dos = new DataOutputStream(outputStream); + this.webappPath = webappPath; + } + + public void forward(String path) throws IOException { + String filePath = webappPath + path; + byte[] fileContent = Files.readAllBytes(Paths.get(filePath)); + String contentType = ContentType.fromFileExtension(filePath).getValue(); + + writeStatusLine(HttpStatus.OK); + writeHeader(HttpHeader.CONTENT_TYPE, contentType); + writeHeader(HttpHeader.CONTENT_LENGTH, String.valueOf(fileContent.length)); + writeEndOfHeaders(); + writeBody(fileContent); + } + + public void redirect(String path) throws IOException { + writeStatusLine(HttpStatus.FOUND); + writeHeader(HttpHeader.LOCATION, path); + writeEndOfHeaders(); + } + + public void redirectWithCookie(String path, String cookieValue) throws IOException { + writeStatusLine(HttpStatus.FOUND); + writeHeader(HttpHeader.SET_COOKIE, cookieValue); + writeHeader(HttpHeader.LOCATION, path); + writeEndOfHeaders(); + } + + public void notFound() throws IOException { + String errorMessage = "404 Not Found"; + byte[] errorBody = errorMessage.getBytes(); + + writeStatusLine(HttpStatus.NOT_FOUND); + writeHeader(HttpHeader.CONTENT_TYPE, ContentType.TEXT_HTML.getValue()); + writeHeader(HttpHeader.CONTENT_LENGTH, String.valueOf(errorBody.length)); + writeEndOfHeaders(); + writeBody(errorBody); + } + + private void writeStatusLine(HttpStatus status) throws IOException { + dos.writeBytes(status.getStatusLine() + "\r\n"); + } + + private void writeHeader(HttpHeader header, String value) throws IOException { + dos.writeBytes(header.getValue() + ": " + value + "\r\n"); + } + + private void writeEndOfHeaders() throws IOException { + dos.writeBytes("\r\n"); + } + + private void writeBody(byte[] body) throws IOException { + dos.write(body, 0, body.length); + dos.flush(); + } +} \ No newline at end of file From 070db5f2eb08d457c0c4c2fceeb0a12a6b5bc8fb Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:19:59 +0900 Subject: [PATCH 17/41] [Feat] Assignment2 Requirement 3: Add HttpResponse unit tests and test HTML file --- src/test/java/HttpResponseTest.java | 100 ++++++++++++++++++++++++++ src/test/resources/response/test.html | 10 +++ 2 files changed, 110 insertions(+) create mode 100644 src/test/java/HttpResponseTest.java create mode 100644 src/test/resources/response/test.html diff --git a/src/test/java/HttpResponseTest.java b/src/test/java/HttpResponseTest.java new file mode 100644 index 0000000..f7b8cc3 --- /dev/null +++ b/src/test/java/HttpResponseTest.java @@ -0,0 +1,100 @@ +import http.HttpResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.*; + +public class HttpResponseTest { + + private static final String TEST_DIRECTORY = "src/test/resources/response/"; + + private OutputStream outputStreamToFile(String path) throws IOException { + return Files.newOutputStream(Paths.get(path)); + } + + @Test + @DisplayName("forward 메서드로 HTML 파일을 전송할 때 올바른 HTTP 응답이 생성되어야 한다") + public void forwardHtmlFileWithCorrectHttpResponse() throws IOException { + // given + String outputPath = TEST_DIRECTORY + "forward_output.txt"; + String testWebappPath = "src/test/resources/response"; + HttpResponse httpResponse = new HttpResponse(outputStreamToFile(outputPath), testWebappPath); + + // when + httpResponse.forward("/test.html"); + + // then + String responseContent = Files.readString(Paths.get(outputPath)); + assertTrue(responseContent.contains("HTTP/1.1 200 OK")); + assertTrue(responseContent.contains("Content-Type: text/html")); + assertTrue(responseContent.contains("Content-Length:")); + assertTrue(responseContent.contains("Test HTML Content")); + + // cleanup + Files.deleteIfExists(Paths.get(outputPath)); + } + + @Test + @DisplayName("redirect 메서드로 리다이렉트할 때 302 응답과 Location 헤더가 생성되어야 한다") + public void redirectWithCorrectLocationHeader() throws IOException { + // given + String outputPath = TEST_DIRECTORY + "redirect_output.txt"; + HttpResponse httpResponse = new HttpResponse(outputStreamToFile(outputPath)); + + // when + httpResponse.redirect("/index.html"); + + // then + String responseContent = Files.readString(Paths.get(outputPath)); + assertTrue(responseContent.contains("HTTP/1.1 302 Found")); + assertTrue(responseContent.contains("Location: /index.html")); + + // cleanup + Files.deleteIfExists(Paths.get(outputPath)); + } + + @Test + @DisplayName("쿠키와 함께 리다이렉트할 때 Set-Cookie 헤더가 포함되어야 한다") + public void redirectWithCookieIncludesSetCookieHeader() throws IOException { + // given + String outputPath = TEST_DIRECTORY + "redirect_cookie_output.txt"; + HttpResponse httpResponse = new HttpResponse(outputStreamToFile(outputPath)); + + // when + httpResponse.redirectWithCookie("/index.html", "logined=true"); + + // then + String responseContent = Files.readString(Paths.get(outputPath)); + assertTrue(responseContent.contains("HTTP/1.1 302 Found")); + assertTrue(responseContent.contains("Set-Cookie: logined=true")); + assertTrue(responseContent.contains("Location: /index.html")); + + // cleanup + Files.deleteIfExists(Paths.get(outputPath)); + } + + @Test + @DisplayName("404 에러 응답을 생성할 때 올바른 에러 메시지가 포함되어야 한다") + public void notFoundResponseWithErrorMessage() throws IOException { + // given + String outputPath = TEST_DIRECTORY + "notfound_output.txt"; + HttpResponse httpResponse = new HttpResponse(outputStreamToFile(outputPath)); + + // when + httpResponse.notFound(); + + // then + String responseContent = Files.readString(Paths.get(outputPath)); + assertTrue(responseContent.contains("HTTP/1.1 404 Not Found")); + assertTrue(responseContent.contains("Content-Type: text/html")); + assertTrue(responseContent.contains("404 Not Found")); + + // cleanup + Files.deleteIfExists(Paths.get(outputPath)); + } +} \ No newline at end of file diff --git a/src/test/resources/response/test.html b/src/test/resources/response/test.html new file mode 100644 index 0000000..ca01255 --- /dev/null +++ b/src/test/resources/response/test.html @@ -0,0 +1,10 @@ + + + + Test Page + + +

Test HTML Content

+

This is a test page for HttpResponse testing.

+ + \ No newline at end of file From 1a43fbe77fd6c70ac17bf6bf974878326309a4bf Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:20:18 +0900 Subject: [PATCH 18/41] [Chore] Assignment2 Requirement 3: Move request test files to request subdirectory --- src/test/java/HttpRequestTest.java | 4 ++-- src/test/resources/{ => request}/get_request.txt | 0 src/test/resources/{ => request}/post_request.txt | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/test/resources/{ => request}/get_request.txt (100%) rename src/test/resources/{ => request}/post_request.txt (100%) diff --git a/src/test/java/HttpRequestTest.java b/src/test/java/HttpRequestTest.java index 1b7b5c5..f9b1510 100644 --- a/src/test/java/HttpRequestTest.java +++ b/src/test/java/HttpRequestTest.java @@ -23,7 +23,7 @@ private BufferedReader bufferedReaderFromFile(String path) throws IOException { @DisplayName("POST 요청을 파싱할 때 메서드, URL, 헤더, 바디가 올바르게 파싱되어야 한다") public void parsePostRequestWithBodyAndHeaders() throws IOException { // given - BufferedReader br = bufferedReaderFromFile(TEST_DIRECTORY + "post_request.txt"); + BufferedReader br = bufferedReaderFromFile(TEST_DIRECTORY + "request/post_request.txt"); // when HttpRequest httpRequest = HttpRequest.from(br); @@ -42,7 +42,7 @@ public void parsePostRequestWithBodyAndHeaders() throws IOException { @DisplayName("GET 요청을 파싱할 때 쿼리 파라미터와 쿠키가 올바르게 파싱되어야 한다") public void parseGetRequestWithQueryParametersAndCookies() throws IOException { // given - BufferedReader br = bufferedReaderFromFile(TEST_DIRECTORY + "get_request.txt"); + BufferedReader br = bufferedReaderFromFile(TEST_DIRECTORY + "request/get_request.txt"); // when HttpRequest httpRequest = HttpRequest.from(br); diff --git a/src/test/resources/get_request.txt b/src/test/resources/request/get_request.txt similarity index 100% rename from src/test/resources/get_request.txt rename to src/test/resources/request/get_request.txt diff --git a/src/test/resources/post_request.txt b/src/test/resources/request/post_request.txt similarity index 100% rename from src/test/resources/post_request.txt rename to src/test/resources/request/post_request.txt From afc5b7b32f896ef365fc26156d28bd224a9ea392 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:20:32 +0900 Subject: [PATCH 19/41] [Feat] Assignment2 Requirement 3: Refactor RequestHandler to use HttpResponse --- src/main/java/webserver/RequestHandler.java | 80 ++++----------------- 1 file changed, 12 insertions(+), 68 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 1908bd1..af67ee8 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -11,6 +11,7 @@ import db.MemoryUserRepository; import http.util.HttpRequestUtils; import http.HttpRequest; +import http.HttpResponse; import model.User; import http.enums.HttpMethod; import http.enums.HttpHeader; @@ -32,7 +33,7 @@ public void run() { log.log(Level.INFO, "New Client Connect! Connected IP : " + connection.getInetAddress() + ", Port : " + connection.getPort()); try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()){ BufferedReader br = new BufferedReader(new InputStreamReader(in)); - DataOutputStream dos = new DataOutputStream(out); + HttpResponse httpResponse = new HttpResponse(out); // HttpRequest 객체로 HTTP 요청 파싱 HttpRequest httpRequest = HttpRequest.from(br); @@ -82,7 +83,7 @@ public void run() { log.log(Level.INFO, "New User Registered: " + newUser.getUserId()); // 302 리다이렉트로 메인 페이지로 이동 - response302Header(dos, RequestPath.INDEX.getValue()); + httpResponse.redirect(RequestPath.INDEX.getValue()); return; } @@ -103,12 +104,12 @@ public void run() { if (user != null && user.getPassword().equals(password)) { // 로그인 성공: Cookie 설정 + 메인페이지로 리다이렉트 log.log(Level.INFO, "Login successful: " + userId); - response302HeaderWithCookie(dos, RequestPath.INDEX.getValue(), "logined=true"); + httpResponse.redirectWithCookie(RequestPath.INDEX.getValue(), "logined=true"); return; } else { // 로그인 실패: 에러페이지로 리다이렉트 log.log(Level.WARNING, "Login failed: " + userId); - response302Header(dos, RequestPath.USER_LOGIN_FAILED.getValue()); + httpResponse.redirect(RequestPath.USER_LOGIN_FAILED.getValue()); return; } } @@ -121,7 +122,7 @@ public void run() { path = RequestPath.USER_LIST_HTML.getValue(); } else { // 비로그인 상태 - response302Header(dos, RequestPath.USER_LOGIN_HTML.getValue()); + httpResponse.redirect(RequestPath.USER_LOGIN_HTML.getValue()); return; } } @@ -138,28 +139,15 @@ public void run() { return; } - // 3. webapp 폴더 기준으로 실제 파일 경로 생성 - String filePath = "webapp" + path; - log.log(Level.INFO, "File path: " + filePath); - try { - // 4. 파일 존재 여부 확인 및 읽기 - byte[] fileContent = Files.readAllBytes(Paths.get(filePath)); - log.log(Level.INFO, "File read successfully: " + filePath); - - // 5. 성공적으로 읽었으면 200 OK 응답 - String contentType = getContentType(filePath); - response200Header(dos, fileContent.length, contentType); - responseBody(dos, fileContent); + // 3. 파일 forward + httpResponse.forward(path); + log.log(Level.INFO, "File forwarded successfully: " + path); } catch (IOException fileException) { - // 6. 파일이 없거나 읽기 실패시 로그 기록 - log.log(Level.WARNING, "File not found or read error: " + filePath); - // TODO: 404 에러 응답 구현 - byte[] errorBody = "404 Not Found".getBytes(); - - response200Header(dos, errorBody.length, ContentType.TEXT_HTML.getValue()); - responseBody(dos, errorBody); + // 4. 파일이 없거나 읽기 실패시 404 에러 응답 + log.log(Level.WARNING, "File not found or read error: " + path); + httpResponse.notFound(); } } catch (IOException e) { @@ -167,48 +155,4 @@ public void run() { } } - private void response200Header(DataOutputStream dos, int lengthOfBodyContent, String contentType) { - try { - dos.writeBytes(HttpStatus.OK.getStatusLine() + " \r\n"); - dos.writeBytes(HttpHeader.CONTENT_TYPE.getValue() + ": " + contentType + "\r\n"); - dos.writeBytes(HttpHeader.CONTENT_LENGTH.getValue() + ": " + lengthOfBodyContent + "\r\n"); - dos.writeBytes("\r\n"); - } catch (IOException e) { - log.log(Level.SEVERE, e.getMessage()); - } - } - - private void response302Header(DataOutputStream dos, String redirectPath) { - try { - dos.writeBytes(HttpStatus.FOUND.getStatusLine() + "\r\n"); - dos.writeBytes(HttpHeader.LOCATION.getValue() + ": " + redirectPath + "\r\n"); - dos.writeBytes("\r\n"); - } catch (IOException e) { - log.log(Level.SEVERE, e.getMessage()); - } - } - - private void response302HeaderWithCookie(DataOutputStream dos, String redirectPath, String cookieValue) { - try { - dos.writeBytes(HttpStatus.FOUND.getStatusLine() + "\r\n"); - dos.writeBytes(HttpHeader.SET_COOKIE.getValue() + ": " + cookieValue + "\r\n"); - dos.writeBytes(HttpHeader.LOCATION.getValue() + ": " + redirectPath + "\r\n"); - dos.writeBytes("\r\n"); - } catch (IOException e) { - log.log(Level.SEVERE, e.getMessage()); - } - } - - private String getContentType(String filePath) { - return ContentType.fromFileExtension(filePath).getValue(); - } - - private void responseBody(DataOutputStream dos, byte[] body) { - try { - dos.write(body, 0, body.length); - dos.flush(); - } catch (IOException e) { - log.log(Level.SEVERE, e.getMessage()); - } - } } \ No newline at end of file From 3cbbf7a4a3f2dce48c8103c3f9271dd665d692a2 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:23:04 +0900 Subject: [PATCH 20/41] [Feat] Assignment2 Requirement 4: Create Controller Interface --- src/main/java/controller/Controller.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/main/java/controller/Controller.java diff --git a/src/main/java/controller/Controller.java b/src/main/java/controller/Controller.java new file mode 100644 index 0000000..6a9e57e --- /dev/null +++ b/src/main/java/controller/Controller.java @@ -0,0 +1,10 @@ +package controller; + +import http.HttpRequest; +import http.HttpResponse; + +import java.io.IOException; + +public interface Controller { + void execute(HttpRequest request, HttpResponse response) throws IOException; +} \ No newline at end of file From d91bd20d4b624bcf8e56aefee664c9317b296060 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:39:39 +0900 Subject: [PATCH 21/41] [Chore] add mockito dependency --- .idea/inspectionProfiles/Project_Default.xml | 8 ++++++++ build.gradle | 2 ++ 2 files changed, 10 insertions(+) create mode 100644 .idea/inspectionProfiles/Project_Default.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..8fc0ff4 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1f1ee97..e8eed4e 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,8 @@ repositories { dependencies { testImplementation platform('org.junit:junit-bom:5.10.0') testImplementation 'org.junit.jupiter:junit-jupiter' + + testImplementation 'org.mockito:mockito-junit-jupiter:5.12.0' } test { From 81328f3c2c12c7b5ca5080003ec25d95c589d536 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:40:03 +0900 Subject: [PATCH 22/41] [Feat] Assignment2 Requirement 4: Create UserSignupControllerTest --- .../controller/UserSignupControllerTest.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/test/java/controller/UserSignupControllerTest.java diff --git a/src/test/java/controller/UserSignupControllerTest.java b/src/test/java/controller/UserSignupControllerTest.java new file mode 100644 index 0000000..f55d4f5 --- /dev/null +++ b/src/test/java/controller/UserSignupControllerTest.java @@ -0,0 +1,81 @@ +package controller; + +import db.MemoryUserRepository; +import http.HttpRequest; +import http.HttpResponse; +import http.enums.HttpMethod; +import http.enums.RequestPath; +import model.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class UserSignupControllerTest { + + @Mock + private HttpRequest mockRequest; + + @Mock + private HttpResponse mockResponse; + + private UserSignupController controller; + private MemoryUserRepository repository; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + controller = new UserSignupController(); + repository = MemoryUserRepository.getInstance(); + } + + @Test + @DisplayName("POST 요청으로 유효한 사용자 정보를 전송하면 회원가입이 성공해야 한다") + public void signupSuccessWithPostRequest() throws IOException { + // given + when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); + when(mockRequest.getBody()).thenReturn("userId=testuser&password=1234&name=홍길동&email=test@example.com"); + when(mockRequest.getQueryString()).thenReturn(null); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).redirect(RequestPath.INDEX.getValue()); + + User savedUser = repository.findUserById("testuser"); + assertNotNull(savedUser, "사용자가 저장되어야 한다"); + assertEquals("testuser", savedUser.getUserId()); + assertEquals("1234", savedUser.getPassword()); + assertEquals("홍길동", savedUser.getName()); + assertEquals("test@example.com", savedUser.getEmail()); + } + + @Test + @DisplayName("POST 요청으로 다른 유효한 사용자 정보를 전송하면 회원가입이 성공해야 한다") + public void signupSuccessWithAnotherPostRequest() throws IOException { + // given + when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); + when(mockRequest.getBody()).thenReturn("userId=getuser&password=5678&name=김철수&email=get@example.com"); + when(mockRequest.getQueryString()).thenReturn(null); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).redirect(RequestPath.INDEX.getValue()); + + User savedUser = repository.findUserById("getuser"); + assertNotNull(savedUser, "사용자가 저장되어야 한다"); + assertEquals("getuser", savedUser.getUserId()); + assertEquals("5678", savedUser.getPassword()); + assertEquals("김철수", savedUser.getName()); + assertEquals("get@example.com", savedUser.getEmail()); + } +} \ No newline at end of file From af032a392f84af297d801d07bb108adfb583354a Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:40:13 +0900 Subject: [PATCH 23/41] [Feat] Assignment2 Requirement 4: Create UserSignupController --- .../java/controller/UserSignupController.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/main/java/controller/UserSignupController.java diff --git a/src/main/java/controller/UserSignupController.java b/src/main/java/controller/UserSignupController.java new file mode 100644 index 0000000..fad4e65 --- /dev/null +++ b/src/main/java/controller/UserSignupController.java @@ -0,0 +1,44 @@ +package controller; + +import db.MemoryUserRepository; +import http.HttpRequest; +import http.HttpResponse; +import http.enums.HttpMethod; +import http.enums.RequestPath; +import http.util.HttpRequestUtils; +import model.User; +import model.UserField; + +import java.io.IOException; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class UserSignupController implements Controller { + private static final Logger log = Logger.getLogger(UserSignupController.class.getName()); + + @Override + public void execute(HttpRequest request, HttpResponse response) throws IOException { + Map params; + + // body에서 파라미터 추출 + params = HttpRequestUtils.parseQueryParameter(request.getBody()); + log.log(Level.INFO, "POST Signup params: " + params); + + // User 객체 생성 + User newUser = new User( + params.get(UserField.USER_ID.getValue()), + params.get(UserField.PASSWORD.getValue()), + params.get(UserField.NAME.getValue()), + params.get(UserField.EMAIL.getValue()) + ); + + // 메모리 저장소에 저장 + MemoryUserRepository repository = MemoryUserRepository.getInstance(); + repository.addUser(newUser); + log.log(Level.INFO, "New User Registered: " + newUser.getUserId()); + + // 302 리다이렉트로 메인 페이지로 이동 + response.redirect(RequestPath.INDEX.getValue()); + } +} \ No newline at end of file From 766093abede3904d4bdaa28e96ab1c10c86b56f0 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:40:31 +0900 Subject: [PATCH 24/41] [Refact] Assignment2 Requirement 4: Refact RequestHandler to using UserSignupController --- src/main/java/webserver/RequestHandler.java | 36 +++------------------ 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index af67ee8..b14a8a0 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -8,15 +8,13 @@ import java.util.logging.Level; import java.util.logging.Logger; +import controller.UserSignupController; import db.MemoryUserRepository; import http.util.HttpRequestUtils; import http.HttpRequest; import http.HttpResponse; import model.User; import http.enums.HttpMethod; -import http.enums.HttpHeader; -import http.enums.HttpStatus; -import http.enums.ContentType; import http.enums.RequestPath; import model.UserField; @@ -55,35 +53,9 @@ public void run() { // 경로에 따른 파일 매핑 로직 // 회원 가입 처리 - if (path.equals(RequestPath.USER_SIGNUP.getValue()) && (queryString != null || HttpMethod.POST.getValue().equals(method))) { - // queryString parsing - Map params; - - if (HttpMethod.POST.getValue().equals(method)) { - // body에서 파라미터 추출 - params = HttpRequestUtils.parseQueryParameter(requestBody); - log.log(Level.INFO, "POST Signup params: " + params); - } else { - // GET에서 파라미터 추출 - params = HttpRequestUtils.parseQueryParameter(queryString); - log.log(Level.INFO, "GET Signup params: " + params); - } - - // User 객체 생성 - User newUser = new User( - params.get(UserField.USER_ID.getValue()), - params.get(UserField.PASSWORD.getValue()), - params.get(UserField.NAME.getValue()), - params.get(UserField.EMAIL.getValue()) - ); - - // 메모리 저장소에 저장 - MemoryUserRepository repository = MemoryUserRepository.getInstance(); - repository.addUser(newUser); - log.log(Level.INFO, "New User Registered: " + newUser.getUserId()); - - // 302 리다이렉트로 메인 페이지로 이동 - httpResponse.redirect(RequestPath.INDEX.getValue()); + if (path.equals(RequestPath.USER_SIGNUP.getValue()) && HttpMethod.POST.getValue().equals(method)) { + UserSignupController controller = new UserSignupController(); + controller.execute(httpRequest, httpResponse); return; } From 201e646202ec88adea6ac6ed4262ff7deb6f2531 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:43:20 +0900 Subject: [PATCH 25/41] [Feat] Assignment2 Requirement 4: Create UserLoginControllerTest --- .../controller/UserLoginControllerTest.java | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/test/java/controller/UserLoginControllerTest.java diff --git a/src/test/java/controller/UserLoginControllerTest.java b/src/test/java/controller/UserLoginControllerTest.java new file mode 100644 index 0000000..32fbeb7 --- /dev/null +++ b/src/test/java/controller/UserLoginControllerTest.java @@ -0,0 +1,96 @@ +package controller; + +import db.MemoryUserRepository; +import http.HttpRequest; +import http.HttpResponse; +import http.enums.HttpMethod; +import http.enums.RequestPath; +import model.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; + +import static org.mockito.Mockito.*; + +public class UserLoginControllerTest { + + @Mock + private HttpRequest mockRequest; + + @Mock + private HttpResponse mockResponse; + + private UserLoginController controller; + private MemoryUserRepository repository; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + controller = new UserLoginController(); + repository = MemoryUserRepository.getInstance(); + + // 테스트용 사용자 미리 등록 + User testUser = new User("testuser", "1234", "홍길동", "test@example.com"); + repository.addUser(testUser); + } + + @Test + @DisplayName("올바른 계정 정보로 로그인하면 메인 페이지로 리다이렉트되어야 한다") + public void loginSuccessWithValidCredentials() throws IOException { + // given + when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); + when(mockRequest.getBody()).thenReturn("userId=testuser&password=1234"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).redirectWithCookie(RequestPath.INDEX.getValue(), "logined=true"); + } + + @Test + @DisplayName("잘못된 비밀번호로 로그인하면 로그인 실패 페이지로 리다이렉트되어야 한다") + public void loginFailWithWrongPassword() throws IOException { + // given + when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); + when(mockRequest.getBody()).thenReturn("userId=testuser&password=wrongpassword"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).redirect(RequestPath.USER_LOGIN_FAILED.getValue()); + } + + @Test + @DisplayName("존재하지 않는 사용자로 로그인하면 로그인 실패 페이지로 리다이렉트되어야 한다") + public void loginFailWithNonExistentUser() throws IOException { + // given + when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); + when(mockRequest.getBody()).thenReturn("userId=nonexistent&password=1234"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).redirect(RequestPath.USER_LOGIN_FAILED.getValue()); + } + + @Test + @DisplayName("빈 파라미터로 로그인하면 로그인 실패 페이지로 리다이렉트되어야 한다") + public void loginFailWithEmptyParameters() throws IOException { + // given + when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); + when(mockRequest.getBody()).thenReturn(""); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).redirect(RequestPath.USER_LOGIN_FAILED.getValue()); + } +} \ No newline at end of file From 4d4c1dad97eff6223a0b57679e4b55406b30c185 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:43:28 +0900 Subject: [PATCH 26/41] [Feat] Assignment2 Requirement 4: Create UserLoginController --- .../java/controller/UserLoginController.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/java/controller/UserLoginController.java diff --git a/src/main/java/controller/UserLoginController.java b/src/main/java/controller/UserLoginController.java new file mode 100644 index 0000000..7c88ce4 --- /dev/null +++ b/src/main/java/controller/UserLoginController.java @@ -0,0 +1,49 @@ +package controller; + +import db.MemoryUserRepository; +import http.HttpRequest; +import http.HttpResponse; +import http.enums.RequestPath; +import http.util.HttpRequestUtils; +import model.User; +import model.UserField; + +import java.io.IOException; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class UserLoginController implements Controller { + private static final Logger log = Logger.getLogger(UserLoginController.class.getName()); + + @Override + public void execute(HttpRequest request, HttpResponse response) throws IOException { + Map params = HttpRequestUtils.parseQueryParameter(request.getBody()); + log.log(Level.INFO, "Login params: " + params); + + String userId = params.get(UserField.USER_ID.getValue()); + String password = params.get(UserField.PASSWORD.getValue()); + + // 파라미터 유효성 검사 + if (userId == null || userId.isEmpty() || password == null || password.isEmpty()) { + log.log(Level.WARNING, "Login failed: missing parameters"); + response.redirect(RequestPath.USER_LOGIN_FAILED.getValue()); + return; + } + + // MemoryUserRepository에서 사용자 조회 + MemoryUserRepository repository = MemoryUserRepository.getInstance(); + User user = repository.findUserById(userId); + + // 인증 검증 + if (user != null && user.getPassword().equals(password)) { + // 로그인 성공: Cookie 설정 + 메인페이지로 리다이렉트 + log.log(Level.INFO, "Login successful: " + userId); + response.redirectWithCookie(RequestPath.INDEX.getValue(), "logined=true"); + } else { + // 로그인 실패: 에러페이지로 리다이렉트 + log.log(Level.WARNING, "Login failed: " + userId); + response.redirect(RequestPath.USER_LOGIN_FAILED.getValue()); + } + } +} \ No newline at end of file From 9cad2f71d097efda80a615aca6908f4ef8b4655d Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:46:47 +0900 Subject: [PATCH 27/41] [Refact] Assignment2 Requirement 4: Refact RequestHandler to using UserLoginController --- src/main/java/webserver/RequestHandler.java | 33 ++++----------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index b14a8a0..e0b0a03 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -9,14 +9,11 @@ import java.util.logging.Logger; import controller.UserSignupController; -import db.MemoryUserRepository; -import http.util.HttpRequestUtils; +import controller.UserLoginController; import http.HttpRequest; import http.HttpResponse; -import model.User; import http.enums.HttpMethod; import http.enums.RequestPath; -import model.UserField; public class RequestHandler implements Runnable{ Socket connection; @@ -59,31 +56,11 @@ public void run() { return; } - // 로그인 처리 + // 로그인 처리 (POST 방식만) if (path.equals(RequestPath.USER_LOGIN.getValue()) && HttpMethod.POST.getValue().equals(method)) { - // POST 방식의 로그인만 처리 - Map params = HttpRequestUtils.parseQueryParameter(requestBody); - log.log(Level.INFO, "Login params: " + params); - - String userId = params.get(UserField.USER_ID.getValue()); - String password = params.get(UserField.PASSWORD.getValue()); - - // MemoryUserRepository에서 사용자 조회 - MemoryUserRepository repository = MemoryUserRepository.getInstance(); - User user = repository.findUserById(userId); - - // 인증 검증 - if (user != null && user.getPassword().equals(password)) { - // 로그인 성공: Cookie 설정 + 메인페이지로 리다이렉트 - log.log(Level.INFO, "Login successful: " + userId); - httpResponse.redirectWithCookie(RequestPath.INDEX.getValue(), "logined=true"); - return; - } else { - // 로그인 실패: 에러페이지로 리다이렉트 - log.log(Level.WARNING, "Login failed: " + userId); - httpResponse.redirect(RequestPath.USER_LOGIN_FAILED.getValue()); - return; - } + UserLoginController controller = new UserLoginController(); + controller.execute(httpRequest, httpResponse); + return; } // userList 경로 처리 From b77b4796c01edcef727b170dbb0d263ddeec73bf Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:49:02 +0900 Subject: [PATCH 28/41] [Feat] Assignment2 Requirement 4: Create UserListController --- .../controller/UserListControllerTest.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/test/java/controller/UserListControllerTest.java diff --git a/src/test/java/controller/UserListControllerTest.java b/src/test/java/controller/UserListControllerTest.java new file mode 100644 index 0000000..6bc5c09 --- /dev/null +++ b/src/test/java/controller/UserListControllerTest.java @@ -0,0 +1,97 @@ +package controller; + +import http.HttpRequest; +import http.HttpResponse; +import http.enums.HttpMethod; +import http.enums.RequestPath; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; + +import static org.mockito.Mockito.*; + +public class UserListControllerTest { + + @Mock + private HttpRequest mockRequest; + + @Mock + private HttpResponse mockResponse; + + private UserListController controller; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + controller = new UserListController(); + } + + @Test + @DisplayName("로그인된 사용자가 사용자 목록에 접근하면 사용자 목록 페이지를 보여줘야 한다") + public void showUserListForLoggedInUser() throws IOException { + // given + when(mockRequest.getCookie()).thenReturn("logined=true"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).forward(RequestPath.USER_LIST_HTML.getValue()); + } + + @Test + @DisplayName("로그인되지 않은 사용자가 사용자 목록에 접근하면 로그인 페이지로 리다이렉트되어야 한다") + public void redirectToLoginForNonLoggedInUser() throws IOException { + // given + when(mockRequest.getCookie()).thenReturn(null); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).redirect(RequestPath.USER_LOGIN_HTML.getValue()); + } + + @Test + @DisplayName("다른 쿠키가 있지만 로그인 쿠키가 없는 사용자는 로그인 페이지로 리다이렉트되어야 한다") + public void redirectToLoginForUserWithOtherCookies() throws IOException { + // given + when(mockRequest.getCookie()).thenReturn("other=value; session=abc123"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).redirect(RequestPath.USER_LOGIN_HTML.getValue()); + } + + @Test + @DisplayName("logined=false 쿠키가 있는 사용자는 로그인 페이지로 리다이렉트되어야 한다") + public void redirectToLoginForUserWithLoginedFalseCookie() throws IOException { + // given + when(mockRequest.getCookie()).thenReturn("logined=false"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).redirect(RequestPath.USER_LOGIN_HTML.getValue()); + } + + @Test + @DisplayName("logined=true를 포함한 복합 쿠키가 있는 사용자는 사용자 목록을 볼 수 있어야 한다") + public void showUserListForUserWithComplexCookieIncludingLogined() throws IOException { + // given + when(mockRequest.getCookie()).thenReturn("session=abc123; logined=true; theme=dark"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).forward(RequestPath.USER_LIST_HTML.getValue()); + } +} \ No newline at end of file From 5b96d7b25282bc62c9dbde8a4ab19ceca4ed2233 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:49:14 +0900 Subject: [PATCH 29/41] [Feat] Assignment2 Requirement 4: Create UserListController --- .../java/controller/UserListController.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/main/java/controller/UserListController.java diff --git a/src/main/java/controller/UserListController.java b/src/main/java/controller/UserListController.java new file mode 100644 index 0000000..9b2ba13 --- /dev/null +++ b/src/main/java/controller/UserListController.java @@ -0,0 +1,29 @@ +package controller; + +import http.HttpRequest; +import http.HttpResponse; +import http.enums.RequestPath; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class UserListController implements Controller { + private static final Logger log = Logger.getLogger(UserListController.class.getName()); + + @Override + public void execute(HttpRequest request, HttpResponse response) throws IOException { + String cookieValue = request.getCookie(); + + // Cookie에서 로그인 상태 확인 + if (cookieValue != null && cookieValue.contains("logined=true")) { + // 로그인된 사용자: user/list.html 파일 forward + log.log(Level.INFO, "Logged in user accessing user list"); + response.forward(RequestPath.USER_LIST_HTML.getValue()); + } else { + // 비로그인 상태: 로그인 페이지로 리다이렉트 + log.log(Level.INFO, "Non-logged user redirected to login page"); + response.redirect(RequestPath.USER_LOGIN_HTML.getValue()); + } + } +} \ No newline at end of file From 25ed0b9248a3634e133c07ad167b9530a9b0baf1 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:49:30 +0900 Subject: [PATCH 30/41] [Refact] Assignment2 Requirement 4: Refact RequestHandler to using UserListController --- src/main/java/webserver/RequestHandler.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index e0b0a03..ea36501 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -10,6 +10,7 @@ import controller.UserSignupController; import controller.UserLoginController; +import controller.UserListController; import http.HttpRequest; import http.HttpResponse; import http.enums.HttpMethod; @@ -65,15 +66,9 @@ public void run() { // userList 경로 처리 if (path.equals(RequestPath.USER_LIST.getValue())) { - // Cookie 에서 로그인 상태 확인 - if (cookieValue != null && cookieValue.contains("logined=true")) { - // user/list.html 파일 - path = RequestPath.USER_LIST_HTML.getValue(); - } else { - // 비로그인 상태 - httpResponse.redirect(RequestPath.USER_LOGIN_HTML.getValue()); - return; - } + UserListController controller = new UserListController(); + controller.execute(httpRequest, httpResponse); + return; } // 1. 루트 경로 ("/") 처리 - 기본 페이지로 리다이렉트 From 53f75b671bc0938e47f1f905af0e2eb0a40d6cf9 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:53:29 +0900 Subject: [PATCH 31/41] [Feat] Assignment2 Requirement 4: Create StaticFileControllerTest --- .../controller/StaticFileControllerTest.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 src/test/java/controller/StaticFileControllerTest.java diff --git a/src/test/java/controller/StaticFileControllerTest.java b/src/test/java/controller/StaticFileControllerTest.java new file mode 100644 index 0000000..887c107 --- /dev/null +++ b/src/test/java/controller/StaticFileControllerTest.java @@ -0,0 +1,112 @@ +package controller; + +import http.HttpRequest; +import http.HttpResponse; +import http.enums.RequestPath; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; + +import static org.mockito.Mockito.*; + +public class StaticFileControllerTest { + + @Mock + private HttpRequest mockRequest; + + @Mock + private HttpResponse mockResponse; + + private StaticFileController controller; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + controller = new StaticFileController(); + } + + @Test + @DisplayName("루트 경로(/)로 요청하면 인덱스 페이지로 forward되어야 한다") + public void forwardToIndexForRootPath() throws IOException { + // given + when(mockRequest.getPath()).thenReturn("/"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).forward(RequestPath.INDEX.getValue()); + } + + @Test + @DisplayName("일반 정적 파일 경로로 요청하면 해당 파일로 forward되어야 한다") + public void forwardToStaticFile() throws IOException { + // given + when(mockRequest.getPath()).thenReturn("/css/style.css"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).forward("/css/style.css"); + } + + @Test + @DisplayName("디렉토리 트래버설 공격(..)이 포함된 경로는 404 응답을 해야 한다") + public void returnNotFoundForDirectoryTraversalAttack() throws IOException { + // given + when(mockRequest.getPath()).thenReturn("/../../etc/passwd"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).notFound(); + verify(mockResponse, never()).forward(anyString()); + } + + @Test + @DisplayName("파일이 존재하지 않으면 404 응답을 해야 한다") + public void returnNotFoundForNonExistentFile() throws IOException { + // given + when(mockRequest.getPath()).thenReturn("/nonexistent.html"); + doThrow(new IOException("File not found")).when(mockResponse).forward("/nonexistent.html"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).forward("/nonexistent.html"); + verify(mockResponse).notFound(); + } + + @Test + @DisplayName("HTML 파일 요청은 정상적으로 forward되어야 한다") + public void forwardHtmlFile() throws IOException { + // given + when(mockRequest.getPath()).thenReturn("/user/form.html"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).forward("/user/form.html"); + } + + @Test + @DisplayName("JS 파일 요청은 정상적으로 forward되어야 한다") + public void forwardJsFile() throws IOException { + // given + when(mockRequest.getPath()).thenReturn("/js/app.js"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).forward("/js/app.js"); + } +} \ No newline at end of file From 4e926f58d2a06d6bc3e2d4c0b50d2ae567232ded Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:53:38 +0900 Subject: [PATCH 32/41] [Feat] Assignment2 Requirement 4: Create StaticFileController --- .../java/controller/StaticFileController.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/java/controller/StaticFileController.java diff --git a/src/main/java/controller/StaticFileController.java b/src/main/java/controller/StaticFileController.java new file mode 100644 index 0000000..1faa6b1 --- /dev/null +++ b/src/main/java/controller/StaticFileController.java @@ -0,0 +1,41 @@ +package controller; + +import http.HttpRequest; +import http.HttpResponse; +import http.enums.RequestPath; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class StaticFileController implements Controller { + private static final Logger log = Logger.getLogger(StaticFileController.class.getName()); + + @Override + public void execute(HttpRequest request, HttpResponse response) throws IOException { + String path = request.getPath(); + + // 1. 루트 경로 ("/") 처리 - 기본 페이지로 변경 + if (path.equals(RequestPath.ROOT.getValue())) { + path = RequestPath.INDEX.getValue(); + } + + // 2. 보안 검증 - ../ 과 같은 디렉토리 traversal 공격 방지 + if (path.contains("..")) { + log.log(Level.WARNING, "Directory traversal attack detected: " + path); + response.notFound(); + return; + } + + try { + // 3. 파일 forward + response.forward(path); + log.log(Level.INFO, "Static file served successfully: " + path); + + } catch (IOException fileException) { + // 4. 파일이 없거나 읽기 실패시 404 에러 응답 + log.log(Level.WARNING, "File not found or read error: " + path); + response.notFound(); + } + } +} \ No newline at end of file From 24aaf94d7869b9719e3f22f27bc81402862f62ef Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 17:53:56 +0900 Subject: [PATCH 33/41] [Refact] Assignment2 Requirement 4: Refact RequestHandler to using StaticFileController --- src/main/java/webserver/RequestHandler.java | 26 ++++----------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index ea36501..a4ca7c8 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -11,6 +11,7 @@ import controller.UserSignupController; import controller.UserLoginController; import controller.UserListController; +import controller.StaticFileController; import http.HttpRequest; import http.HttpResponse; import http.enums.HttpMethod; @@ -71,28 +72,9 @@ public void run() { return; } - // 1. 루트 경로 ("/") 처리 - 기본 페이지로 리다이렉트 - if (path.equals(RequestPath.ROOT.getValue())) { - path = RequestPath.INDEX.getValue(); - } - - // 2. 보안 검증 - ../ 과 같은 디렉토리 traversal 공격 방지 - if (path.contains("..")) { - log.log(Level.WARNING, "Path contains invalid path: " + path); - // TODO: 에러 반환 - return; - } - - try { - // 3. 파일 forward - httpResponse.forward(path); - log.log(Level.INFO, "File forwarded successfully: " + path); - - } catch (IOException fileException) { - // 4. 파일이 없거나 읽기 실패시 404 에러 응답 - log.log(Level.WARNING, "File not found or read error: " + path); - httpResponse.notFound(); - } + // 정적 파일 처리 (모든 나머지 요청) + StaticFileController controller = new StaticFileController(); + controller.execute(httpRequest, httpResponse); } catch (IOException e) { log.log(Level.SEVERE,e.getMessage()); From cd5e736210e5b557edac8ebd074d6f407fd92b23 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 18:00:51 +0900 Subject: [PATCH 34/41] [Chore] Change StaticFileController File Name to ForwardController --- .../{StaticFileController.java => ForwardController.java} | 4 ++-- ...icFileControllerTest.java => ForwardControllerTest.java} | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename src/main/java/controller/{StaticFileController.java => ForwardController.java} (88%) rename src/test/java/controller/{StaticFileControllerTest.java => ForwardControllerTest.java} (95%) diff --git a/src/main/java/controller/StaticFileController.java b/src/main/java/controller/ForwardController.java similarity index 88% rename from src/main/java/controller/StaticFileController.java rename to src/main/java/controller/ForwardController.java index 1faa6b1..e5627b8 100644 --- a/src/main/java/controller/StaticFileController.java +++ b/src/main/java/controller/ForwardController.java @@ -8,8 +8,8 @@ import java.util.logging.Level; import java.util.logging.Logger; -public class StaticFileController implements Controller { - private static final Logger log = Logger.getLogger(StaticFileController.class.getName()); +public class ForwardController implements Controller { + private static final Logger log = Logger.getLogger(ForwardController.class.getName()); @Override public void execute(HttpRequest request, HttpResponse response) throws IOException { diff --git a/src/test/java/controller/StaticFileControllerTest.java b/src/test/java/controller/ForwardControllerTest.java similarity index 95% rename from src/test/java/controller/StaticFileControllerTest.java rename to src/test/java/controller/ForwardControllerTest.java index 887c107..805fbf8 100644 --- a/src/test/java/controller/StaticFileControllerTest.java +++ b/src/test/java/controller/ForwardControllerTest.java @@ -13,7 +13,7 @@ import static org.mockito.Mockito.*; -public class StaticFileControllerTest { +public class ForwardControllerTest { @Mock private HttpRequest mockRequest; @@ -21,12 +21,12 @@ public class StaticFileControllerTest { @Mock private HttpResponse mockResponse; - private StaticFileController controller; + private ForwardController controller; @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); - controller = new StaticFileController(); + controller = new ForwardController(); } @Test From 822abc51e57c89f7f0c82e60185ef55aacc6e509 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 18:01:02 +0900 Subject: [PATCH 35/41] [Refact] Refact RequestHandler --- src/main/java/webserver/RequestHandler.java | 51 ++++++--------------- 1 file changed, 14 insertions(+), 37 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index a4ca7c8..cbd0ec6 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -2,16 +2,10 @@ import java.io.*; import java.net.Socket; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; -import controller.UserSignupController; -import controller.UserLoginController; -import controller.UserListController; -import controller.StaticFileController; +import controller.*; import http.HttpRequest; import http.HttpResponse; import http.enums.HttpMethod; @@ -20,6 +14,7 @@ public class RequestHandler implements Runnable{ Socket connection; private static final Logger log = Logger.getLogger(RequestHandler.class.getName()); + private Controller controller = new ForwardController(); public RequestHandler(Socket connection) { this.connection = connection; @@ -36,44 +31,26 @@ public void run() { HttpRequest httpRequest = HttpRequest.from(br); log.log(Level.INFO, "Request Line: " + httpRequest.getMethod() + " " + httpRequest.getPath() + " " + httpRequest.getVersion()); - String method = httpRequest.getMethod().getValue(); - String path = httpRequest.getPath(); - String queryString = httpRequest.getQueryString(); - String cookieValue = httpRequest.getCookie(); - String requestBody = httpRequest.getBody(); - - log.log(Level.INFO, "Method: " + method + ", Path: " + path + ", Query String: " + queryString); - if (cookieValue != null) { - log.log(Level.INFO, "Cookie received: " + cookieValue); - } - if (requestBody != null) { - log.log(Level.INFO, "Request body: " + requestBody); + // Controller 선택 로직 + if (httpRequest.getPath().equals(RequestPath.ROOT.getValue())) { + controller = new ForwardController(); } - // 경로에 따른 파일 매핑 로직 - // 회원 가입 처리 - if (path.equals(RequestPath.USER_SIGNUP.getValue()) && HttpMethod.POST.getValue().equals(method)) { - UserSignupController controller = new UserSignupController(); - controller.execute(httpRequest, httpResponse); - return; + if (httpRequest.getPath().equals(RequestPath.USER_SIGNUP.getValue()) && + httpRequest.getMethod() == HttpMethod.POST) { + controller = new UserSignupController(); } - // 로그인 처리 (POST 방식만) - if (path.equals(RequestPath.USER_LOGIN.getValue()) && HttpMethod.POST.getValue().equals(method)) { - UserLoginController controller = new UserLoginController(); - controller.execute(httpRequest, httpResponse); - return; + if (httpRequest.getPath().equals(RequestPath.USER_LOGIN.getValue()) && + httpRequest.getMethod() == HttpMethod.POST) { + controller = new UserLoginController(); } - // userList 경로 처리 - if (path.equals(RequestPath.USER_LIST.getValue())) { - UserListController controller = new UserListController(); - controller.execute(httpRequest, httpResponse); - return; + if (httpRequest.getPath().equals(RequestPath.USER_LIST.getValue())) { + controller = new UserListController(); } - // 정적 파일 처리 (모든 나머지 요청) - StaticFileController controller = new StaticFileController(); + // Controller 실행 controller.execute(httpRequest, httpResponse); } catch (IOException e) { From 2b6e29234eb3e3ec4ce814792908c38950982f97 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 18:07:47 +0900 Subject: [PATCH 36/41] [Feat] Assignment2 Requirement 5: Create RequestMapper --- src/main/java/webserver/RequestMapper.java | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/main/java/webserver/RequestMapper.java diff --git a/src/main/java/webserver/RequestMapper.java b/src/main/java/webserver/RequestMapper.java new file mode 100644 index 0000000..39be3da --- /dev/null +++ b/src/main/java/webserver/RequestMapper.java @@ -0,0 +1,58 @@ +package webserver; + +import controller.*; +import http.HttpRequest; +import http.HttpResponse; +import http.enums.HttpMethod; +import http.enums.RequestPath; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class RequestMapper { + private final HttpRequest httpRequest; + private final HttpResponse httpResponse; + private final Map controllers; + + public RequestMapper(HttpRequest httpRequest, HttpResponse httpResponse) { + this.httpRequest = httpRequest; + this.httpResponse = httpResponse; + this.controllers = new HashMap<>(); + initializeControllers(); + } + + private void initializeControllers() { + controllers.put(createKey(RequestPath.ROOT.getValue(), null), new ForwardController()); + controllers.put(createKey(RequestPath.USER_SIGNUP.getValue(), HttpMethod.POST), new UserSignupController()); + controllers.put(createKey(RequestPath.USER_LOGIN.getValue(), HttpMethod.POST), new UserLoginController()); + controllers.put(createKey(RequestPath.USER_LIST.getValue(), null), new UserListController()); + } + + private String createKey(String path, HttpMethod method) { + if (method == null) { + return path; + } + return path + "_" + method.name(); + } + + public void proceed() throws IOException { + String path = httpRequest.getPath(); + HttpMethod method = httpRequest.getMethod(); + + Controller controller = getController(path, method); + if (controller == null) { + controller = new ForwardController(); + } + + controller.execute(httpRequest, httpResponse); + } + + private Controller getController(String path, HttpMethod method) { + Controller controller = controllers.get(createKey(path, method)); + if (controller == null) { + controller = controllers.get(path); + } + return controller; + } +} \ No newline at end of file From 6a3600761c44c078e17e7abc5d23e902d1c63759 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 18:08:04 +0900 Subject: [PATCH 37/41] [Refact] Assignment2 Requirement 5: Refact RequestHandler to using RequestMapper --- src/main/java/webserver/RequestHandler.java | 49 +++++---------------- 1 file changed, 12 insertions(+), 37 deletions(-) diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index cbd0ec6..6c908f1 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -1,20 +1,17 @@ package webserver; +import http.HttpRequest; +import http.HttpResponse; + import java.io.*; import java.net.Socket; +import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; -import controller.*; -import http.HttpRequest; -import http.HttpResponse; -import http.enums.HttpMethod; -import http.enums.RequestPath; - -public class RequestHandler implements Runnable{ +public class RequestHandler implements Runnable { Socket connection; private static final Logger log = Logger.getLogger(RequestHandler.class.getName()); - private Controller controller = new ForwardController(); public RequestHandler(Socket connection) { this.connection = connection; @@ -23,39 +20,17 @@ public RequestHandler(Socket connection) { @Override public void run() { log.log(Level.INFO, "New Client Connect! Connected IP : " + connection.getInetAddress() + ", Port : " + connection.getPort()); - try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()){ + try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) { BufferedReader br = new BufferedReader(new InputStreamReader(in)); - HttpResponse httpResponse = new HttpResponse(out); - - // HttpRequest 객체로 HTTP 요청 파싱 HttpRequest httpRequest = HttpRequest.from(br); - log.log(Level.INFO, "Request Line: " + httpRequest.getMethod() + " " + httpRequest.getPath() + " " + httpRequest.getVersion()); - - // Controller 선택 로직 - if (httpRequest.getPath().equals(RequestPath.ROOT.getValue())) { - controller = new ForwardController(); - } - - if (httpRequest.getPath().equals(RequestPath.USER_SIGNUP.getValue()) && - httpRequest.getMethod() == HttpMethod.POST) { - controller = new UserSignupController(); - } - - if (httpRequest.getPath().equals(RequestPath.USER_LOGIN.getValue()) && - httpRequest.getMethod() == HttpMethod.POST) { - controller = new UserLoginController(); - } - - if (httpRequest.getPath().equals(RequestPath.USER_LIST.getValue())) { - controller = new UserListController(); - } + HttpResponse httpResponse = new HttpResponse(out); - // Controller 실행 - controller.execute(httpRequest, httpResponse); + RequestMapper requestMapper = new RequestMapper(httpRequest,httpResponse); + requestMapper.proceed(); - } catch (IOException e) { - log.log(Level.SEVERE,e.getMessage()); + } catch (Exception e) { + log.log(Level.SEVERE, e.getMessage()); + System.out.println(Arrays.toString(e.getStackTrace())); } } - } \ No newline at end of file From 6db6245952f2dcadbf4bdece63b76aa968486e4c Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Mon, 29 Sep 2025 18:08:16 +0900 Subject: [PATCH 38/41] [Feat] Assignment2 Requirement 5: Create RequestMapperTest --- .../java/webserver/RequestMapperTest.java | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/test/java/webserver/RequestMapperTest.java diff --git a/src/test/java/webserver/RequestMapperTest.java b/src/test/java/webserver/RequestMapperTest.java new file mode 100644 index 0000000..ec25d1c --- /dev/null +++ b/src/test/java/webserver/RequestMapperTest.java @@ -0,0 +1,109 @@ +package webserver; + +import controller.*; +import http.HttpRequest; +import http.HttpResponse; +import http.enums.HttpMethod; +import http.enums.RequestPath; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; + +import static org.mockito.Mockito.*; + +@DisplayName("RequestMapper 테스트") +class RequestMapperTest { + + @Mock + private HttpRequest mockRequest; + + @Mock + private HttpResponse mockResponse; + + private RequestMapper requestMapper; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + requestMapper = new RequestMapper(mockRequest, mockResponse); + } + + @Test + @DisplayName("루트 경로 요청 시 ForwardController가 실행된다") + void shouldExecuteForwardControllerWhenRootPath() throws IOException { + // given + when(mockRequest.getPath()).thenReturn(RequestPath.ROOT.getValue()); + when(mockRequest.getMethod()).thenReturn(HttpMethod.GET); + + // when + requestMapper.proceed(); + + // then + verify(mockRequest, atLeastOnce()).getPath(); + verify(mockRequest, atLeastOnce()).getMethod(); + } + + @Test + @DisplayName("POST /user/signup 요청 시 UserSignupController가 실행된다") + void shouldExecuteUserSignupControllerWhenPostUserSignup() throws IOException { + // given + when(mockRequest.getPath()).thenReturn(RequestPath.USER_SIGNUP.getValue()); + when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); + + // when + requestMapper.proceed(); + + // then + verify(mockRequest, atLeastOnce()).getPath(); + verify(mockRequest, atLeastOnce()).getMethod(); + } + + @Test + @DisplayName("POST /user/login 요청 시 UserLoginController가 실행된다") + void shouldExecuteUserLoginControllerWhenPostUserLogin() throws IOException { + // given + when(mockRequest.getPath()).thenReturn(RequestPath.USER_LOGIN.getValue()); + when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); + + // when + requestMapper.proceed(); + + // then + verify(mockRequest, atLeastOnce()).getPath(); + verify(mockRequest, atLeastOnce()).getMethod(); + } + + @Test + @DisplayName("/user/userList 요청 시 UserListController가 실행된다") + void shouldExecuteUserListControllerWhenUserList() throws IOException { + // given + when(mockRequest.getPath()).thenReturn(RequestPath.USER_LIST.getValue()); + when(mockRequest.getMethod()).thenReturn(HttpMethod.GET); + + // when + requestMapper.proceed(); + + // then + verify(mockRequest, atLeastOnce()).getPath(); + verify(mockRequest, atLeastOnce()).getMethod(); + } + + @Test + @DisplayName("알 수 없는 경로 요청 시 ForwardController가 기본으로 실행된다") + void shouldExecuteForwardControllerWhenUnknownPath() throws IOException { + // given + when(mockRequest.getPath()).thenReturn("/unknown/path"); + when(mockRequest.getMethod()).thenReturn(HttpMethod.GET); + + // when + requestMapper.proceed(); + + // then + verify(mockRequest, atLeastOnce()).getPath(); + verify(mockRequest, atLeastOnce()).getMethod(); + } +} \ No newline at end of file From 75f58e347e1658ef025d03291d1aec3c4871288e Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Wed, 1 Oct 2025 00:30:57 +0900 Subject: [PATCH 39/41] [Refact] Refactor controller mapping to WebConfig class --- src/main/java/webserver/RequestMapper.java | 34 ++++++------------ src/main/java/webserver/WebConfig.java | 40 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 src/main/java/webserver/WebConfig.java diff --git a/src/main/java/webserver/RequestMapper.java b/src/main/java/webserver/RequestMapper.java index 39be3da..ba9b1b9 100644 --- a/src/main/java/webserver/RequestMapper.java +++ b/src/main/java/webserver/RequestMapper.java @@ -4,55 +4,43 @@ import http.HttpRequest; import http.HttpResponse; import http.enums.HttpMethod; -import http.enums.RequestPath; import java.io.IOException; -import java.util.HashMap; import java.util.Map; public class RequestMapper { private final HttpRequest httpRequest; private final HttpResponse httpResponse; - private final Map controllers; + + private static final Map CONTROLLERS = WebConfig.configureControllers(); public RequestMapper(HttpRequest httpRequest, HttpResponse httpResponse) { this.httpRequest = httpRequest; this.httpResponse = httpResponse; - this.controllers = new HashMap<>(); - initializeControllers(); - } - - private void initializeControllers() { - controllers.put(createKey(RequestPath.ROOT.getValue(), null), new ForwardController()); - controllers.put(createKey(RequestPath.USER_SIGNUP.getValue(), HttpMethod.POST), new UserSignupController()); - controllers.put(createKey(RequestPath.USER_LOGIN.getValue(), HttpMethod.POST), new UserLoginController()); - controllers.put(createKey(RequestPath.USER_LIST.getValue(), null), new UserListController()); - } - - private String createKey(String path, HttpMethod method) { - if (method == null) { - return path; - } - return path + "_" + method.name(); } public void proceed() throws IOException { String path = httpRequest.getPath(); HttpMethod method = httpRequest.getMethod(); - + Controller controller = getController(path, method); if (controller == null) { controller = new ForwardController(); } - + controller.execute(httpRequest, httpResponse); } private Controller getController(String path, HttpMethod method) { - Controller controller = controllers.get(createKey(path, method)); + // 먼저 path + method 조합으로 찾기 + String key = WebConfig.createKey(path, method); + Controller controller = CONTROLLERS.get(key); + + // 없으면 path만으로 찾기 if (controller == null) { - controller = controllers.get(path); + controller = CONTROLLERS.get(path); } + return controller; } } \ No newline at end of file diff --git a/src/main/java/webserver/WebConfig.java b/src/main/java/webserver/WebConfig.java new file mode 100644 index 0000000..9ebe2cf --- /dev/null +++ b/src/main/java/webserver/WebConfig.java @@ -0,0 +1,40 @@ +package webserver; + +import controller.*; +import http.enums.HttpMethod; +import http.enums.RequestPath; + +import java.util.HashMap; +import java.util.Map; + +/** + * Controller 매핑 설정을 담당하는 클래스 + * Spring의 @Configuration과 유사한 역할 + */ +public class WebConfig { + + public static Map configureControllers() { + Map controllers = new HashMap<>(); + + // 루트 경로 + controllers.put(createKey(RequestPath.ROOT.getValue(), null), new ForwardController()); + + // 회원가입 + controllers.put(createKey(RequestPath.USER_SIGNUP.getValue(), HttpMethod.POST), new UserSignupController()); + + // 로그인 + controllers.put(createKey(RequestPath.USER_LOGIN.getValue(), HttpMethod.POST), new UserLoginController()); + + // 회원 목록 + controllers.put(createKey(RequestPath.USER_LIST.getValue(), null), new UserListController()); + + return controllers; + } + + public static String createKey(String path, HttpMethod method) { + if (method == null) { + return path; + } + return path + "_" + method.name(); + } +} \ No newline at end of file From 48e86d8ddb243aff1d4ed390457c45ad61416004 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Fri, 3 Oct 2025 19:17:56 +0900 Subject: [PATCH 40/41] [Fix] CodeRabbit Review --- .idea/vcs.xml | 6 ++++++ .../java/controller/UserListController.java | 13 +++++++++++- .../java/controller/UserLoginController.java | 5 ++++- .../java/controller/UserSignupController.java | 21 +++++++++++++------ src/main/java/http/HttpHeaders.java | 15 +++++++++---- src/main/java/http/HttpRequest.java | 2 +- src/main/java/http/HttpResponse.java | 13 ++++++++++++ src/main/java/http/enums/ContentType.java | 12 +++++++---- src/main/java/http/enums/RequestPath.java | 2 +- src/test/java/HttpRequestTest.java | 4 ++-- .../controller/UserLoginControllerTest.java | 2 +- src/test/resources/request/post_request.txt | 2 +- 12 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 .idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/main/java/controller/UserListController.java b/src/main/java/controller/UserListController.java index 9b2ba13..fdd5cd3 100644 --- a/src/main/java/controller/UserListController.java +++ b/src/main/java/controller/UserListController.java @@ -5,6 +5,7 @@ import http.enums.RequestPath; import java.io.IOException; +import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; @@ -16,7 +17,7 @@ public void execute(HttpRequest request, HttpResponse response) throws IOExcepti String cookieValue = request.getCookie(); // Cookie에서 로그인 상태 확인 - if (cookieValue != null && cookieValue.contains("logined=true")) { + if (cookieValue != null && Objects.equals(parseCookieValue(cookieValue, "logined"), "true")) { // 로그인된 사용자: user/list.html 파일 forward log.log(Level.INFO, "Logged in user accessing user list"); response.forward(RequestPath.USER_LIST_HTML.getValue()); @@ -26,4 +27,14 @@ public void execute(HttpRequest request, HttpResponse response) throws IOExcepti response.redirect(RequestPath.USER_LOGIN_HTML.getValue()); } } + + private String parseCookieValue(String cookie, String key) { + for (String pair : cookie.split(";\\s*")) { + String[] kv = pair.split("=", 2); + if (kv.length == 2 && kv[0].equals(key)) { + return kv[1]; + } + } + return null; + } } \ No newline at end of file diff --git a/src/main/java/controller/UserLoginController.java b/src/main/java/controller/UserLoginController.java index 7c88ce4..3d5fb9a 100644 --- a/src/main/java/controller/UserLoginController.java +++ b/src/main/java/controller/UserLoginController.java @@ -19,11 +19,14 @@ public class UserLoginController implements Controller { @Override public void execute(HttpRequest request, HttpResponse response) throws IOException { Map params = HttpRequestUtils.parseQueryParameter(request.getBody()); - log.log(Level.INFO, "Login params: " + params); String userId = params.get(UserField.USER_ID.getValue()); String password = params.get(UserField.PASSWORD.getValue()); + if (userId != null && !userId.isEmpty()) { + log.log(Level.INFO, "Login attempt: " + userId); + } + // 파라미터 유효성 검사 if (userId == null || userId.isEmpty() || password == null || password.isEmpty()) { log.log(Level.WARNING, "Login failed: missing parameters"); diff --git a/src/main/java/controller/UserSignupController.java b/src/main/java/controller/UserSignupController.java index fad4e65..4a0dc7d 100644 --- a/src/main/java/controller/UserSignupController.java +++ b/src/main/java/controller/UserSignupController.java @@ -25,13 +25,22 @@ public void execute(HttpRequest request, HttpResponse response) throws IOExcepti params = HttpRequestUtils.parseQueryParameter(request.getBody()); log.log(Level.INFO, "POST Signup params: " + params); + // 필수 필드 검증 + String userId = params.get (UserField.USER_ID.getValue()); + String password = params.get (UserField. PASSWORD.getValue()); + String name = params.get (UserField.NAME.getValue()); + String email = params.get (UserField.EMAIL.getValue()); + + if (userId == null || userId.trim().isEmpty() || + password == null || password. length() < 4 || + name == null || name.trim().isEmpty() || + email == null || !email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$")) { + // TODO: Bad Request + return; + } + // User 객체 생성 - User newUser = new User( - params.get(UserField.USER_ID.getValue()), - params.get(UserField.PASSWORD.getValue()), - params.get(UserField.NAME.getValue()), - params.get(UserField.EMAIL.getValue()) - ); + User newUser = new User(userId, password, name, email); // 메모리 저장소에 저장 MemoryUserRepository repository = MemoryUserRepository.getInstance(); diff --git a/src/main/java/http/HttpHeaders.java b/src/main/java/http/HttpHeaders.java index dc6983a..55e27c6 100644 --- a/src/main/java/http/HttpHeaders.java +++ b/src/main/java/http/HttpHeaders.java @@ -21,7 +21,7 @@ public static HttpHeaders from(BufferedReader br) throws IOException { while ((line = br.readLine()) != null && !line.isEmpty()) { int colonIndex = line.indexOf(":"); if (colonIndex != -1) { - String key = line.substring(0, colonIndex).trim(); + String key = line.substring(0, colonIndex).trim().toLowerCase(); String value = line.substring(colonIndex + 1).trim(); headerMap.put(key, value); } @@ -31,16 +31,23 @@ public static HttpHeaders from(BufferedReader br) throws IOException { } public String getHeader(HttpHeader header) { - return headers.get(header.getValue()); + return headers.get(header.getValue().toLowerCase()); } public String getHeader(String headerName) { - return headers.get(headerName); + return headers.get(headerName.toLowerCase()); } public int getContentLength() { String contentLength = getHeader(HttpHeader.CONTENT_LENGTH); - return contentLength != null ? Integer.parseInt(contentLength) : 0; + if (contentLength == null) { + return 0; + } + try { + return Integer.parseInt(contentLength); + } catch (NumberFormatException e) { + return 0; + } } public String getCookie() { diff --git a/src/main/java/http/HttpRequest.java b/src/main/java/http/HttpRequest.java index d8fc54a..e620ca0 100644 --- a/src/main/java/http/HttpRequest.java +++ b/src/main/java/http/HttpRequest.java @@ -39,7 +39,7 @@ public HttpMethod getMethod() { } public String getUrl() { - return startLine.getPathWithoutQuery(); + return startLine.getPath(); } public String getPath() { diff --git a/src/main/java/http/HttpResponse.java b/src/main/java/http/HttpResponse.java index 0387417..8c07ed1 100644 --- a/src/main/java/http/HttpResponse.java +++ b/src/main/java/http/HttpResponse.java @@ -24,7 +24,20 @@ public HttpResponse(OutputStream outputStream, String webappPath) { } public void forward(String path) throws IOException { + // 경로 정규화 및 검증 + if (path.contains("..")) { + notFound(); + return; + } + String filePath = webappPath + path; + + // 파일 존재 확인 + if (!Files.exists(Paths.get(filePath))) { + notFound(); + return; + } + byte[] fileContent = Files.readAllBytes(Paths.get(filePath)); String contentType = ContentType.fromFileExtension(filePath).getValue(); diff --git a/src/main/java/http/enums/ContentType.java b/src/main/java/http/enums/ContentType.java index c867c59..ee8c25e 100644 --- a/src/main/java/http/enums/ContentType.java +++ b/src/main/java/http/enums/ContentType.java @@ -1,5 +1,7 @@ package http.enums; +import java.util.Locale; + public enum ContentType { TEXT_HTML("text/html;charset=utf-8"), TEXT_CSS("text/css"), @@ -18,13 +20,15 @@ public String getValue() { } public static ContentType fromFileExtension(String filePath) { - if (filePath.endsWith(".css")) { + String normalized = filePath.split("\\?")[0].toLowerCase(Locale.ROOT); + + if (normalized.endsWith(".css")) { return TEXT_CSS; - } else if (filePath.endsWith(".js")) { + } else if (normalized.endsWith(".js")) { return APPLICATION_JAVASCRIPT; - } else if (filePath.endsWith(".png")) { + } else if (normalized.endsWith(".png")) { return IMAGE_PNG; - } else if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) { + } else if (normalized.endsWith(".jpg") || normalized.endsWith(".jpeg")) { return IMAGE_JPEG; } else { return TEXT_HTML; diff --git a/src/main/java/http/enums/RequestPath.java b/src/main/java/http/enums/RequestPath.java index ecc0ce3..02170be 100644 --- a/src/main/java/http/enums/RequestPath.java +++ b/src/main/java/http/enums/RequestPath.java @@ -26,6 +26,6 @@ public static RequestPath from(String path) { return requestPath; } } - return null; + throw new IllegalArgumentException("Unknown request path: " + path); } } \ No newline at end of file diff --git a/src/test/java/HttpRequestTest.java b/src/test/java/HttpRequestTest.java index f9b1510..92f6d43 100644 --- a/src/test/java/HttpRequestTest.java +++ b/src/test/java/HttpRequestTest.java @@ -33,7 +33,7 @@ public void parsePostRequestWithBodyAndHeaders() throws IOException { assertEquals("/user/create", httpRequest.getUrl()); assertEquals("HTTP/1.1", httpRequest.getVersion()); assertEquals("localhost:8080", httpRequest.getHeader("Host")); - assertEquals(40, httpRequest.getContentLength()); + assertEquals(35, httpRequest.getContentLength()); assertNotNull(httpRequest.getBody()); assertTrue(httpRequest.getBody().contains("userId")); } @@ -49,7 +49,7 @@ public void parseGetRequestWithQueryParametersAndCookies() throws IOException { // then assertEquals(HttpMethod.GET, httpRequest.getMethod()); - assertEquals("/index.html", httpRequest.getUrl()); + assertEquals("/index.html?name=foden&age=26", httpRequest.getUrl()); assertEquals("name=foden&age=26", httpRequest.getQueryString()); assertEquals("HTTP/1.1", httpRequest.getVersion()); assertEquals("localhost:8080", httpRequest.getHeader("Host")); diff --git a/src/test/java/controller/UserLoginControllerTest.java b/src/test/java/controller/UserLoginControllerTest.java index 32fbeb7..1230323 100644 --- a/src/test/java/controller/UserLoginControllerTest.java +++ b/src/test/java/controller/UserLoginControllerTest.java @@ -32,7 +32,7 @@ public void setUp() { MockitoAnnotations.openMocks(this); controller = new UserLoginController(); repository = MemoryUserRepository.getInstance(); - + // 테스트용 사용자 미리 등록 User testUser = new User("testuser", "1234", "홍길동", "test@example.com"); repository.addUser(testUser); diff --git a/src/test/resources/request/post_request.txt b/src/test/resources/request/post_request.txt index 4ed4004..021b529 100644 --- a/src/test/resources/request/post_request.txt +++ b/src/test/resources/request/post_request.txt @@ -1,7 +1,7 @@ POST /user/create HTTP/1.1 Host: localhost:8080 Connection: keep-alive -Content-Length: 40 +Content-Length: 35 Accept: */* userId=foden&password=0801&name=PSG \ No newline at end of file From 0788b1381debb5b36a685e4bf6584c6ea3e53dbc Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Tue, 7 Oct 2025 18:04:02 +0900 Subject: [PATCH 41/41] [Fix] Code Review --- .../java/controller/UserListController.java | 17 +--- .../java/controller/UserLoginController.java | 3 +- .../java/controller/UserSignupController.java | 7 +- src/main/java/http/HttpHeaders.java | 12 --- src/main/java/http/HttpRequest.java | 27 ++++++ src/main/java/http/HttpResponse.java | 14 ++-- src/main/java/http/HttpStartLine.java | 22 +++-- src/main/java/http/enums/HttpStatus.java | 20 +++-- src/main/java/webserver/RequestHandler.java | 9 +- src/main/java/webserver/RequestMapper.java | 28 +------ src/test/java/HttpResponseTest.java | 12 ++- .../controller/ForwardControllerTest.java | 3 +- .../controller/UserListControllerTest.java | 21 +---- .../controller/UserLoginControllerTest.java | 12 +-- .../controller/UserSignupControllerTest.java | 22 +++-- .../java/webserver/RequestMapperTest.java | 82 ++++++++----------- 16 files changed, 135 insertions(+), 176 deletions(-) diff --git a/src/main/java/controller/UserListController.java b/src/main/java/controller/UserListController.java index fdd5cd3..7ff2789 100644 --- a/src/main/java/controller/UserListController.java +++ b/src/main/java/controller/UserListController.java @@ -5,7 +5,6 @@ import http.enums.RequestPath; import java.io.IOException; -import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; @@ -14,10 +13,10 @@ public class UserListController implements Controller { @Override public void execute(HttpRequest request, HttpResponse response) throws IOException { - String cookieValue = request.getCookie(); - // Cookie에서 로그인 상태 확인 - if (cookieValue != null && Objects.equals(parseCookieValue(cookieValue, "logined"), "true")) { + String logined = request.getCookie("logined"); + + if (logined != null) { // 로그인된 사용자: user/list.html 파일 forward log.log(Level.INFO, "Logged in user accessing user list"); response.forward(RequestPath.USER_LIST_HTML.getValue()); @@ -27,14 +26,4 @@ public void execute(HttpRequest request, HttpResponse response) throws IOExcepti response.redirect(RequestPath.USER_LOGIN_HTML.getValue()); } } - - private String parseCookieValue(String cookie, String key) { - for (String pair : cookie.split(";\\s*")) { - String[] kv = pair.split("=", 2); - if (kv.length == 2 && kv[0].equals(key)) { - return kv[1]; - } - } - return null; - } } \ No newline at end of file diff --git a/src/main/java/controller/UserLoginController.java b/src/main/java/controller/UserLoginController.java index 3d5fb9a..8bdb3e9 100644 --- a/src/main/java/controller/UserLoginController.java +++ b/src/main/java/controller/UserLoginController.java @@ -4,7 +4,6 @@ import http.HttpRequest; import http.HttpResponse; import http.enums.RequestPath; -import http.util.HttpRequestUtils; import model.User; import model.UserField; @@ -18,7 +17,7 @@ public class UserLoginController implements Controller { @Override public void execute(HttpRequest request, HttpResponse response) throws IOException { - Map params = HttpRequestUtils.parseQueryParameter(request.getBody()); + Map params = request.getParameters(); String userId = params.get(UserField.USER_ID.getValue()); String password = params.get(UserField.PASSWORD.getValue()); diff --git a/src/main/java/controller/UserSignupController.java b/src/main/java/controller/UserSignupController.java index 4a0dc7d..5386a61 100644 --- a/src/main/java/controller/UserSignupController.java +++ b/src/main/java/controller/UserSignupController.java @@ -5,7 +5,6 @@ import http.HttpResponse; import http.enums.HttpMethod; import http.enums.RequestPath; -import http.util.HttpRequestUtils; import model.User; import model.UserField; @@ -19,10 +18,8 @@ public class UserSignupController implements Controller { @Override public void execute(HttpRequest request, HttpResponse response) throws IOException { - Map params; - - // body에서 파라미터 추출 - params = HttpRequestUtils.parseQueryParameter(request.getBody()); + // 파라미터 추출 + Map params = request.getParameters(); log.log(Level.INFO, "POST Signup params: " + params); // 필수 필드 검증 diff --git a/src/main/java/http/HttpHeaders.java b/src/main/java/http/HttpHeaders.java index 55e27c6..90903b0 100644 --- a/src/main/java/http/HttpHeaders.java +++ b/src/main/java/http/HttpHeaders.java @@ -53,16 +53,4 @@ public int getContentLength() { public String getCookie() { return getHeader(HttpHeader.COOKIE); } - - public boolean hasHeader(HttpHeader header) { - return headers.containsKey(header.getValue()); - } - - public boolean hasHeader(String headerName) { - return headers.containsKey(headerName); - } - - public Map getAllHeaders() { - return new HashMap<>(headers); - } } \ No newline at end of file diff --git a/src/main/java/http/HttpRequest.java b/src/main/java/http/HttpRequest.java index e620ca0..89c8bae 100644 --- a/src/main/java/http/HttpRequest.java +++ b/src/main/java/http/HttpRequest.java @@ -1,10 +1,13 @@ package http; import http.enums.HttpMethod; +import http.util.HttpRequestUtils; import http.util.IOUtils; import java.io.BufferedReader; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; public class HttpRequest { private final HttpStartLine startLine; @@ -62,6 +65,21 @@ public String getCookie() { return headers.getCookie(); } + public String getCookie(String key) { + String cookieHeader = headers.getCookie(); + if (cookieHeader == null) { + return null; + } + + for (String pair : cookieHeader.split(";\\s*")) { + String[] kv = pair.split("=", 2); + if (kv.length == 2 && kv[0].equals(key)) { + return kv[1]; + } + } + return null; + } + public int getContentLength() { return headers.getContentLength(); } @@ -77,4 +95,13 @@ public HttpStartLine getStartLine() { public HttpHeaders getHeaders() { return headers; } + + public Map getParameters() { + if (startLine.getMethod() == HttpMethod.GET && getQueryString() != null) { + return HttpRequestUtils.parseQueryParameter(getQueryString()); + } else if (startLine.getMethod() == HttpMethod.POST && body != null) { + return HttpRequestUtils.parseQueryParameter(body); + } + return new HashMap<>(); + } } \ No newline at end of file diff --git a/src/main/java/http/HttpResponse.java b/src/main/java/http/HttpResponse.java index 8c07ed1..c6f6f31 100644 --- a/src/main/java/http/HttpResponse.java +++ b/src/main/java/http/HttpResponse.java @@ -11,16 +11,18 @@ import java.nio.file.Paths; public class HttpResponse { + private static final String WEBAPP_PATH = "webapp"; + private static final String DEFAULT_HTTP_VERSION = "HTTP/1.1"; private final DataOutputStream dos; - private final String webappPath; + private final String httpVersion; public HttpResponse(OutputStream outputStream) { - this(outputStream, "webapp"); + this(outputStream, DEFAULT_HTTP_VERSION); } - public HttpResponse(OutputStream outputStream, String webappPath) { + public HttpResponse(OutputStream outputStream, String httpVersion) { this.dos = new DataOutputStream(outputStream); - this.webappPath = webappPath; + this.httpVersion = httpVersion; } public void forward(String path) throws IOException { @@ -30,7 +32,7 @@ public void forward(String path) throws IOException { return; } - String filePath = webappPath + path; + String filePath = WEBAPP_PATH + path; // 파일 존재 확인 if (!Files.exists(Paths.get(filePath))) { @@ -73,7 +75,7 @@ public void notFound() throws IOException { } private void writeStatusLine(HttpStatus status) throws IOException { - dos.writeBytes(status.getStatusLine() + "\r\n"); + dos.writeBytes(status.getStatusLine(httpVersion) + "\r\n"); } private void writeHeader(HttpHeader header, String value) throws IOException { diff --git a/src/main/java/http/HttpStartLine.java b/src/main/java/http/HttpStartLine.java index e2ae3e3..8716e57 100644 --- a/src/main/java/http/HttpStartLine.java +++ b/src/main/java/http/HttpStartLine.java @@ -6,11 +6,23 @@ public class HttpStartLine { private final HttpMethod method; private final String path; private final String version; + private final String pathWithoutQuery; + private final String queryString; public HttpStartLine(HttpMethod method, String path, String version) { this.method = method; this.path = path; this.version = version; + + // 생성 시 한 번만 파싱 + int queryIndex = path.indexOf("?"); + if (queryIndex != -1) { + this.pathWithoutQuery = path.substring(0, queryIndex); + this.queryString = path.substring(queryIndex + 1); + } else { + this.pathWithoutQuery = path; + this.queryString = null; + } } public static HttpStartLine from(String requestLine) { @@ -43,16 +55,10 @@ public String getVersion() { } public String getPathWithoutQuery() { - if (path.contains("?")) { - return path.substring(0, path.indexOf("?")); - } - return path; + return pathWithoutQuery; } public String getQueryString() { - if (path.contains("?")) { - return path.substring(path.indexOf("?") + 1); - } - return null; + return queryString; } } \ No newline at end of file diff --git a/src/main/java/http/enums/HttpStatus.java b/src/main/java/http/enums/HttpStatus.java index 83424ea..e9252cf 100644 --- a/src/main/java/http/enums/HttpStatus.java +++ b/src/main/java/http/enums/HttpStatus.java @@ -1,23 +1,27 @@ package http.enums; public enum HttpStatus { - OK(200, "HTTP/1.1 200 OK"), - FOUND(302, "HTTP/1.1 302 Found"), - NOT_FOUND(404, "HTTP/1.1 404 Not Found"); + OK(200, "OK"), + FOUND(302, "Found"), + NOT_FOUND(404, "Not Found"); private final int code; - private final String statusLine; + private final String reasonPhrase; - HttpStatus(int code, String statusLine) { + HttpStatus(int code, String reasonPhrase) { this.code = code; - this.statusLine = statusLine; + this.reasonPhrase = reasonPhrase; } public int getCode() { return code; } - public String getStatusLine() { - return statusLine; + public String getReasonPhrase() { + return reasonPhrase; + } + + public String getStatusLine(String httpVersion) { + return httpVersion + " " + code + " " + reasonPhrase; } } \ No newline at end of file diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 6c908f1..805a815 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -23,14 +23,15 @@ public void run() { try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) { BufferedReader br = new BufferedReader(new InputStreamReader(in)); HttpRequest httpRequest = HttpRequest.from(br); - HttpResponse httpResponse = new HttpResponse(out); + HttpResponse httpResponse = new HttpResponse(out, httpRequest.getVersion()); - RequestMapper requestMapper = new RequestMapper(httpRequest,httpResponse); - requestMapper.proceed(); + RequestMapper requestMapper = new RequestMapper(); + requestMapper.getController(httpRequest.getPath(), httpRequest.getMethod()) + .execute(httpRequest, httpResponse); } catch (Exception e) { log.log(Level.SEVERE, e.getMessage()); - System.out.println(Arrays.toString(e.getStackTrace())); + log.log(Level.SEVERE, Arrays.toString(e.getStackTrace())); } } } \ No newline at end of file diff --git a/src/main/java/webserver/RequestMapper.java b/src/main/java/webserver/RequestMapper.java index ba9b1b9..0fd7b33 100644 --- a/src/main/java/webserver/RequestMapper.java +++ b/src/main/java/webserver/RequestMapper.java @@ -1,37 +1,14 @@ package webserver; import controller.*; -import http.HttpRequest; -import http.HttpResponse; import http.enums.HttpMethod; -import java.io.IOException; import java.util.Map; public class RequestMapper { - private final HttpRequest httpRequest; - private final HttpResponse httpResponse; - private static final Map CONTROLLERS = WebConfig.configureControllers(); - public RequestMapper(HttpRequest httpRequest, HttpResponse httpResponse) { - this.httpRequest = httpRequest; - this.httpResponse = httpResponse; - } - - public void proceed() throws IOException { - String path = httpRequest.getPath(); - HttpMethod method = httpRequest.getMethod(); - - Controller controller = getController(path, method); - if (controller == null) { - controller = new ForwardController(); - } - - controller.execute(httpRequest, httpResponse); - } - - private Controller getController(String path, HttpMethod method) { + public Controller getController(String path, HttpMethod method) { // 먼저 path + method 조합으로 찾기 String key = WebConfig.createKey(path, method); Controller controller = CONTROLLERS.get(key); @@ -41,6 +18,7 @@ private Controller getController(String path, HttpMethod method) { controller = CONTROLLERS.get(path); } - return controller; + // 매핑된 컨트롤러가 없으면 ForwardController 반환 + return controller != null ? controller : new ForwardController(); } } \ No newline at end of file diff --git a/src/test/java/HttpResponseTest.java b/src/test/java/HttpResponseTest.java index f7b8cc3..b88315f 100644 --- a/src/test/java/HttpResponseTest.java +++ b/src/test/java/HttpResponseTest.java @@ -22,19 +22,17 @@ private OutputStream outputStreamToFile(String path) throws IOException { public void forwardHtmlFileWithCorrectHttpResponse() throws IOException { // given String outputPath = TEST_DIRECTORY + "forward_output.txt"; - String testWebappPath = "src/test/resources/response"; - HttpResponse httpResponse = new HttpResponse(outputStreamToFile(outputPath), testWebappPath); - + HttpResponse httpResponse = new HttpResponse(outputStreamToFile(outputPath)); + // when - httpResponse.forward("/test.html"); - + httpResponse.forward("/index.html"); + // then String responseContent = Files.readString(Paths.get(outputPath)); assertTrue(responseContent.contains("HTTP/1.1 200 OK")); assertTrue(responseContent.contains("Content-Type: text/html")); assertTrue(responseContent.contains("Content-Length:")); - assertTrue(responseContent.contains("Test HTML Content")); - + // cleanup Files.deleteIfExists(Paths.get(outputPath)); } diff --git a/src/test/java/controller/ForwardControllerTest.java b/src/test/java/controller/ForwardControllerTest.java index 805fbf8..5d7daca 100644 --- a/src/test/java/controller/ForwardControllerTest.java +++ b/src/test/java/controller/ForwardControllerTest.java @@ -21,12 +21,11 @@ public class ForwardControllerTest { @Mock private HttpResponse mockResponse; - private ForwardController controller; + private final ForwardController controller = new ForwardController(); @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); - controller = new ForwardController(); } @Test diff --git a/src/test/java/controller/UserListControllerTest.java b/src/test/java/controller/UserListControllerTest.java index 6bc5c09..2548972 100644 --- a/src/test/java/controller/UserListControllerTest.java +++ b/src/test/java/controller/UserListControllerTest.java @@ -34,7 +34,7 @@ public void setUp() { @DisplayName("로그인된 사용자가 사용자 목록에 접근하면 사용자 목록 페이지를 보여줘야 한다") public void showUserListForLoggedInUser() throws IOException { // given - when(mockRequest.getCookie()).thenReturn("logined=true"); + when(mockRequest.getCookie("logined")).thenReturn("true"); // when controller.execute(mockRequest, mockResponse); @@ -47,7 +47,7 @@ public void showUserListForLoggedInUser() throws IOException { @DisplayName("로그인되지 않은 사용자가 사용자 목록에 접근하면 로그인 페이지로 리다이렉트되어야 한다") public void redirectToLoginForNonLoggedInUser() throws IOException { // given - when(mockRequest.getCookie()).thenReturn(null); + when(mockRequest.getCookie("logined")).thenReturn(null); // when controller.execute(mockRequest, mockResponse); @@ -60,20 +60,7 @@ public void redirectToLoginForNonLoggedInUser() throws IOException { @DisplayName("다른 쿠키가 있지만 로그인 쿠키가 없는 사용자는 로그인 페이지로 리다이렉트되어야 한다") public void redirectToLoginForUserWithOtherCookies() throws IOException { // given - when(mockRequest.getCookie()).thenReturn("other=value; session=abc123"); - - // when - controller.execute(mockRequest, mockResponse); - - // then - verify(mockResponse).redirect(RequestPath.USER_LOGIN_HTML.getValue()); - } - - @Test - @DisplayName("logined=false 쿠키가 있는 사용자는 로그인 페이지로 리다이렉트되어야 한다") - public void redirectToLoginForUserWithLoginedFalseCookie() throws IOException { - // given - when(mockRequest.getCookie()).thenReturn("logined=false"); + when(mockRequest.getCookie("logined")).thenReturn(null); // when controller.execute(mockRequest, mockResponse); @@ -86,7 +73,7 @@ public void redirectToLoginForUserWithLoginedFalseCookie() throws IOException { @DisplayName("logined=true를 포함한 복합 쿠키가 있는 사용자는 사용자 목록을 볼 수 있어야 한다") public void showUserListForUserWithComplexCookieIncludingLogined() throws IOException { // given - when(mockRequest.getCookie()).thenReturn("session=abc123; logined=true; theme=dark"); + when(mockRequest.getCookie("logined")).thenReturn("true"); // when controller.execute(mockRequest, mockResponse); diff --git a/src/test/java/controller/UserLoginControllerTest.java b/src/test/java/controller/UserLoginControllerTest.java index 1230323..6249e0f 100644 --- a/src/test/java/controller/UserLoginControllerTest.java +++ b/src/test/java/controller/UserLoginControllerTest.java @@ -42,8 +42,7 @@ public void setUp() { @DisplayName("올바른 계정 정보로 로그인하면 메인 페이지로 리다이렉트되어야 한다") public void loginSuccessWithValidCredentials() throws IOException { // given - when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); - when(mockRequest.getBody()).thenReturn("userId=testuser&password=1234"); + when(mockRequest.getParameters()).thenReturn(java.util.Map.of("userId", "testuser", "password", "1234")); // when controller.execute(mockRequest, mockResponse); @@ -56,8 +55,7 @@ public void loginSuccessWithValidCredentials() throws IOException { @DisplayName("잘못된 비밀번호로 로그인하면 로그인 실패 페이지로 리다이렉트되어야 한다") public void loginFailWithWrongPassword() throws IOException { // given - when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); - when(mockRequest.getBody()).thenReturn("userId=testuser&password=wrongpassword"); + when(mockRequest.getParameters()).thenReturn(java.util.Map.of("userId", "testuser", "password", "wrongpassword")); // when controller.execute(mockRequest, mockResponse); @@ -70,8 +68,7 @@ public void loginFailWithWrongPassword() throws IOException { @DisplayName("존재하지 않는 사용자로 로그인하면 로그인 실패 페이지로 리다이렉트되어야 한다") public void loginFailWithNonExistentUser() throws IOException { // given - when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); - when(mockRequest.getBody()).thenReturn("userId=nonexistent&password=1234"); + when(mockRequest.getParameters()).thenReturn(java.util.Map.of("userId", "nonexistent", "password", "1234")); // when controller.execute(mockRequest, mockResponse); @@ -84,8 +81,7 @@ public void loginFailWithNonExistentUser() throws IOException { @DisplayName("빈 파라미터로 로그인하면 로그인 실패 페이지로 리다이렉트되어야 한다") public void loginFailWithEmptyParameters() throws IOException { // given - when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); - when(mockRequest.getBody()).thenReturn(""); + when(mockRequest.getParameters()).thenReturn(java.util.Map.of()); // when controller.execute(mockRequest, mockResponse); diff --git a/src/test/java/controller/UserSignupControllerTest.java b/src/test/java/controller/UserSignupControllerTest.java index f55d4f5..ce9b42f 100644 --- a/src/test/java/controller/UserSignupControllerTest.java +++ b/src/test/java/controller/UserSignupControllerTest.java @@ -39,16 +39,19 @@ public void setUp() { @DisplayName("POST 요청으로 유효한 사용자 정보를 전송하면 회원가입이 성공해야 한다") public void signupSuccessWithPostRequest() throws IOException { // given - when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); - when(mockRequest.getBody()).thenReturn("userId=testuser&password=1234&name=홍길동&email=test@example.com"); - when(mockRequest.getQueryString()).thenReturn(null); + when(mockRequest.getParameters()).thenReturn(java.util.Map.of( + "userId", "testuser", + "password", "1234", + "name", "홍길동", + "email", "test@example.com" + )); // when controller.execute(mockRequest, mockResponse); // then verify(mockResponse).redirect(RequestPath.INDEX.getValue()); - + User savedUser = repository.findUserById("testuser"); assertNotNull(savedUser, "사용자가 저장되어야 한다"); assertEquals("testuser", savedUser.getUserId()); @@ -61,16 +64,19 @@ public void signupSuccessWithPostRequest() throws IOException { @DisplayName("POST 요청으로 다른 유효한 사용자 정보를 전송하면 회원가입이 성공해야 한다") public void signupSuccessWithAnotherPostRequest() throws IOException { // given - when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); - when(mockRequest.getBody()).thenReturn("userId=getuser&password=5678&name=김철수&email=get@example.com"); - when(mockRequest.getQueryString()).thenReturn(null); + when(mockRequest.getParameters()).thenReturn(java.util.Map.of( + "userId", "getuser", + "password", "5678", + "name", "김철수", + "email", "get@example.com" + )); // when controller.execute(mockRequest, mockResponse); // then verify(mockResponse).redirect(RequestPath.INDEX.getValue()); - + User savedUser = repository.findUserById("getuser"); assertNotNull(savedUser, "사용자가 저장되어야 한다"); assertEquals("getuser", savedUser.getUserId()); diff --git a/src/test/java/webserver/RequestMapperTest.java b/src/test/java/webserver/RequestMapperTest.java index ec25d1c..c63c2e5 100644 --- a/src/test/java/webserver/RequestMapperTest.java +++ b/src/test/java/webserver/RequestMapperTest.java @@ -1,109 +1,91 @@ package webserver; import controller.*; -import http.HttpRequest; -import http.HttpResponse; import http.enums.HttpMethod; import http.enums.RequestPath; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import java.io.IOException; - -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; @DisplayName("RequestMapper 테스트") class RequestMapperTest { - @Mock - private HttpRequest mockRequest; - - @Mock - private HttpResponse mockResponse; - private RequestMapper requestMapper; @BeforeEach void setUp() { - MockitoAnnotations.openMocks(this); - requestMapper = new RequestMapper(mockRequest, mockResponse); + requestMapper = new RequestMapper(); } @Test - @DisplayName("루트 경로 요청 시 ForwardController가 실행된다") - void shouldExecuteForwardControllerWhenRootPath() throws IOException { + @DisplayName("루트 경로 요청 시 ForwardController를 반환한다") + void shouldReturnForwardControllerWhenRootPath() { // given - when(mockRequest.getPath()).thenReturn(RequestPath.ROOT.getValue()); - when(mockRequest.getMethod()).thenReturn(HttpMethod.GET); + String path = RequestPath.ROOT.getValue(); + HttpMethod method = HttpMethod.GET; // when - requestMapper.proceed(); + Controller controller = requestMapper.getController(path, method); // then - verify(mockRequest, atLeastOnce()).getPath(); - verify(mockRequest, atLeastOnce()).getMethod(); + assertInstanceOf(ForwardController.class, controller); } @Test - @DisplayName("POST /user/signup 요청 시 UserSignupController가 실행된다") - void shouldExecuteUserSignupControllerWhenPostUserSignup() throws IOException { + @DisplayName("POST /user/signup 요청 시 UserSignupController를 반환한다") + void shouldReturnUserSignupControllerWhenPostUserSignup() { // given - when(mockRequest.getPath()).thenReturn(RequestPath.USER_SIGNUP.getValue()); - when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); + String path = RequestPath.USER_SIGNUP.getValue(); + HttpMethod method = HttpMethod.POST; // when - requestMapper.proceed(); + Controller controller = requestMapper.getController(path, method); // then - verify(mockRequest, atLeastOnce()).getPath(); - verify(mockRequest, atLeastOnce()).getMethod(); + assertInstanceOf(UserSignupController.class, controller); } @Test - @DisplayName("POST /user/login 요청 시 UserLoginController가 실행된다") - void shouldExecuteUserLoginControllerWhenPostUserLogin() throws IOException { + @DisplayName("POST /user/login 요청 시 UserLoginController를 반환한다") + void shouldReturnUserLoginControllerWhenPostUserLogin() { // given - when(mockRequest.getPath()).thenReturn(RequestPath.USER_LOGIN.getValue()); - when(mockRequest.getMethod()).thenReturn(HttpMethod.POST); + String path = RequestPath.USER_LOGIN.getValue(); + HttpMethod method = HttpMethod.POST; // when - requestMapper.proceed(); + Controller controller = requestMapper.getController(path, method); // then - verify(mockRequest, atLeastOnce()).getPath(); - verify(mockRequest, atLeastOnce()).getMethod(); + assertInstanceOf(UserLoginController.class, controller); } @Test - @DisplayName("/user/userList 요청 시 UserListController가 실행된다") - void shouldExecuteUserListControllerWhenUserList() throws IOException { + @DisplayName("/user/userList 요청 시 UserListController를 반환한다") + void shouldReturnUserListControllerWhenUserList() { // given - when(mockRequest.getPath()).thenReturn(RequestPath.USER_LIST.getValue()); - when(mockRequest.getMethod()).thenReturn(HttpMethod.GET); + String path = RequestPath.USER_LIST.getValue(); + HttpMethod method = HttpMethod.GET; // when - requestMapper.proceed(); + Controller controller = requestMapper.getController(path, method); // then - verify(mockRequest, atLeastOnce()).getPath(); - verify(mockRequest, atLeastOnce()).getMethod(); + assertInstanceOf(UserListController.class, controller); } @Test - @DisplayName("알 수 없는 경로 요청 시 ForwardController가 기본으로 실행된다") - void shouldExecuteForwardControllerWhenUnknownPath() throws IOException { + @DisplayName("알 수 없는 경로 요청 시 ForwardController를 기본으로 반환한다") + void shouldReturnForwardControllerWhenUnknownPath() { // given - when(mockRequest.getPath()).thenReturn("/unknown/path"); - when(mockRequest.getMethod()).thenReturn(HttpMethod.GET); + String path = "/unknown/path"; + HttpMethod method = HttpMethod.GET; // when - requestMapper.proceed(); + Controller controller = requestMapper.getController(path, method); // then - verify(mockRequest, atLeastOnce()).getPath(); - verify(mockRequest, atLeastOnce()).getMethod(); + assertInstanceOf(ForwardController.class, controller); } } \ No newline at end of file