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/.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/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 { 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 diff --git a/src/main/java/controller/ForwardController.java b/src/main/java/controller/ForwardController.java new file mode 100644 index 0000000..e5627b8 --- /dev/null +++ b/src/main/java/controller/ForwardController.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 ForwardController implements Controller { + private static final Logger log = Logger.getLogger(ForwardController.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 diff --git a/src/main/java/controller/UserListController.java b/src/main/java/controller/UserListController.java new file mode 100644 index 0000000..7ff2789 --- /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 { + // Cookie에서 로그인 상태 확인 + 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()); + } 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 diff --git a/src/main/java/controller/UserLoginController.java b/src/main/java/controller/UserLoginController.java new file mode 100644 index 0000000..8bdb3e9 --- /dev/null +++ b/src/main/java/controller/UserLoginController.java @@ -0,0 +1,51 @@ +package controller; + +import db.MemoryUserRepository; +import http.HttpRequest; +import http.HttpResponse; +import http.enums.RequestPath; +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 = request.getParameters(); + + 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"); + 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 diff --git a/src/main/java/controller/UserSignupController.java b/src/main/java/controller/UserSignupController.java new file mode 100644 index 0000000..5386a61 --- /dev/null +++ b/src/main/java/controller/UserSignupController.java @@ -0,0 +1,50 @@ +package controller; + +import db.MemoryUserRepository; +import http.HttpRequest; +import http.HttpResponse; +import http.enums.HttpMethod; +import http.enums.RequestPath; +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 = request.getParameters(); + 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(userId, password, name, email); + + // 메모리 저장소에 저장 + 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 diff --git a/src/main/java/http/HttpHeaders.java b/src/main/java/http/HttpHeaders.java new file mode 100644 index 0000000..90903b0 --- /dev/null +++ b/src/main/java/http/HttpHeaders.java @@ -0,0 +1,56 @@ +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().toLowerCase(); + 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().toLowerCase()); + } + + public String getHeader(String headerName) { + return headers.get(headerName.toLowerCase()); + } + + public int getContentLength() { + String contentLength = getHeader(HttpHeader.CONTENT_LENGTH); + if (contentLength == null) { + return 0; + } + try { + return Integer.parseInt(contentLength); + } catch (NumberFormatException e) { + return 0; + } + } + + public String getCookie() { + return getHeader(HttpHeader.COOKIE); + } +} \ 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..89c8bae --- /dev/null +++ b/src/main/java/http/HttpRequest.java @@ -0,0 +1,107 @@ +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; + 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.getPath(); + } + + 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 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(); + } + + public String getBody() { + return body; + } + + public HttpStartLine getStartLine() { + return startLine; + } + + 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 new file mode 100644 index 0000000..c6f6f31 --- /dev/null +++ b/src/main/java/http/HttpResponse.java @@ -0,0 +1,93 @@ +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 static final String WEBAPP_PATH = "webapp"; + private static final String DEFAULT_HTTP_VERSION = "HTTP/1.1"; + private final DataOutputStream dos; + private final String httpVersion; + + public HttpResponse(OutputStream outputStream) { + this(outputStream, DEFAULT_HTTP_VERSION); + } + + public HttpResponse(OutputStream outputStream, String httpVersion) { + this.dos = new DataOutputStream(outputStream); + this.httpVersion = httpVersion; + } + + public void forward(String path) throws IOException { + // 경로 정규화 및 검증 + if (path.contains("..")) { + notFound(); + return; + } + + String filePath = WEBAPP_PATH + path; + + // 파일 존재 확인 + if (!Files.exists(Paths.get(filePath))) { + notFound(); + return; + } + + 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(httpVersion) + "\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 diff --git a/src/main/java/http/HttpStartLine.java b/src/main/java/http/HttpStartLine.java new file mode 100644 index 0000000..8716e57 --- /dev/null +++ b/src/main/java/http/HttpStartLine.java @@ -0,0 +1,64 @@ +package http; + +import http.enums.HttpMethod; + +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) { + 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() { + return pathWithoutQuery; + } + + public String getQueryString() { + return queryString; + } +} \ No newline at end of file diff --git a/src/main/java/http/enums/ContentType.java b/src/main/java/http/enums/ContentType.java new file mode 100644 index 0000000..ee8c25e --- /dev/null +++ b/src/main/java/http/enums/ContentType.java @@ -0,0 +1,37 @@ +package http.enums; + +import java.util.Locale; + +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) { + String normalized = filePath.split("\\?")[0].toLowerCase(Locale.ROOT); + + if (normalized.endsWith(".css")) { + return TEXT_CSS; + } else if (normalized.endsWith(".js")) { + return APPLICATION_JAVASCRIPT; + } else if (normalized.endsWith(".png")) { + return IMAGE_PNG; + } else if (normalized.endsWith(".jpg") || normalized.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..e9252cf --- /dev/null +++ b/src/main/java/http/enums/HttpStatus.java @@ -0,0 +1,27 @@ +package http.enums; + +public enum HttpStatus { + OK(200, "OK"), + FOUND(302, "Found"), + NOT_FOUND(404, "Not Found"); + + private final int code; + private final String reasonPhrase; + + HttpStatus(int code, String reasonPhrase) { + this.code = code; + this.reasonPhrase = reasonPhrase; + } + + public int getCode() { + return code; + } + + public String getReasonPhrase() { + return reasonPhrase; + } + + public String getStatusLine(String httpVersion) { + return httpVersion + " " + code + " " + reasonPhrase; + } +} \ 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..02170be --- /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; + } + } + throw new IllegalArgumentException("Unknown request path: " + path); + } +} \ 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 diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 614a755..805a815 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -1,11 +1,15 @@ 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; -public class RequestHandler implements Runnable{ +public class RequestHandler implements Runnable { Socket connection; private static final Logger log = Logger.getLogger(RequestHandler.class.getName()); @@ -16,37 +20,18 @@ 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)); - DataOutputStream dos = new DataOutputStream(out); + HttpRequest httpRequest = HttpRequest.from(br); + HttpResponse httpResponse = new HttpResponse(out, httpRequest.getVersion()); - byte[] body = "Hello World".getBytes(); - response200Header(dos, body.length); - responseBody(dos, body); + RequestMapper requestMapper = new RequestMapper(); + requestMapper.getController(httpRequest.getPath(), httpRequest.getMethod()) + .execute(httpRequest, httpResponse); - } catch (IOException e) { - log.log(Level.SEVERE,e.getMessage()); - } - } - - private void response200Header(DataOutputStream dos, int lengthOfBodyContent) { - try { - dos.writeBytes("HTTP/1.1 200 OK \r\n"); - dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n"); - dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n"); - dos.writeBytes("\r\n"); - } catch (IOException e) { + } catch (Exception e) { log.log(Level.SEVERE, e.getMessage()); + log.log(Level.SEVERE, Arrays.toString(e.getStackTrace())); } } - - 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 diff --git a/src/main/java/webserver/RequestMapper.java b/src/main/java/webserver/RequestMapper.java new file mode 100644 index 0000000..0fd7b33 --- /dev/null +++ b/src/main/java/webserver/RequestMapper.java @@ -0,0 +1,24 @@ +package webserver; + +import controller.*; +import http.enums.HttpMethod; + +import java.util.Map; + +public class RequestMapper { + private static final Map CONTROLLERS = WebConfig.configureControllers(); + + public Controller getController(String path, HttpMethod method) { + // 먼저 path + method 조합으로 찾기 + String key = WebConfig.createKey(path, method); + Controller controller = CONTROLLERS.get(key); + + // 없으면 path만으로 찾기 + if (controller == null) { + controller = CONTROLLERS.get(path); + } + + // 매핑된 컨트롤러가 없으면 ForwardController 반환 + return controller != null ? controller : new ForwardController(); + } +} \ 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 diff --git a/src/test/java/HttpRequestTest.java b/src/test/java/HttpRequestTest.java new file mode 100644 index 0000000..92f6d43 --- /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 + "request/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(35, httpRequest.getContentLength()); + assertNotNull(httpRequest.getBody()); + assertTrue(httpRequest.getBody().contains("userId")); + } + + @Test + @DisplayName("GET 요청을 파싱할 때 쿼리 파라미터와 쿠키가 올바르게 파싱되어야 한다") + public void parseGetRequestWithQueryParametersAndCookies() throws IOException { + // given + BufferedReader br = bufferedReaderFromFile(TEST_DIRECTORY + "request/get_request.txt"); + + // when + HttpRequest httpRequest = HttpRequest.from(br); + + // then + assertEquals(HttpMethod.GET, httpRequest.getMethod()); + 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")); + 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/java/HttpResponseTest.java b/src/test/java/HttpResponseTest.java new file mode 100644 index 0000000..b88315f --- /dev/null +++ b/src/test/java/HttpResponseTest.java @@ -0,0 +1,98 @@ +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"; + HttpResponse httpResponse = new HttpResponse(outputStreamToFile(outputPath)); + + // when + 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:")); + + // 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/java/controller/ForwardControllerTest.java b/src/test/java/controller/ForwardControllerTest.java new file mode 100644 index 0000000..5d7daca --- /dev/null +++ b/src/test/java/controller/ForwardControllerTest.java @@ -0,0 +1,111 @@ +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 ForwardControllerTest { + + @Mock + private HttpRequest mockRequest; + + @Mock + private HttpResponse mockResponse; + + private final ForwardController controller = new ForwardController(); + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + } + + @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 diff --git a/src/test/java/controller/UserListControllerTest.java b/src/test/java/controller/UserListControllerTest.java new file mode 100644 index 0000000..2548972 --- /dev/null +++ b/src/test/java/controller/UserListControllerTest.java @@ -0,0 +1,84 @@ +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("logined")).thenReturn("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("logined")).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("logined")).thenReturn(null); + + // 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("logined")).thenReturn("true"); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).forward(RequestPath.USER_LIST_HTML.getValue()); + } +} \ No newline at end of file diff --git a/src/test/java/controller/UserLoginControllerTest.java b/src/test/java/controller/UserLoginControllerTest.java new file mode 100644 index 0000000..6249e0f --- /dev/null +++ b/src/test/java/controller/UserLoginControllerTest.java @@ -0,0 +1,92 @@ +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.getParameters()).thenReturn(java.util.Map.of("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.getParameters()).thenReturn(java.util.Map.of("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.getParameters()).thenReturn(java.util.Map.of("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.getParameters()).thenReturn(java.util.Map.of()); + + // when + controller.execute(mockRequest, mockResponse); + + // then + verify(mockResponse).redirect(RequestPath.USER_LOGIN_FAILED.getValue()); + } +} \ No newline at end of file diff --git a/src/test/java/controller/UserSignupControllerTest.java b/src/test/java/controller/UserSignupControllerTest.java new file mode 100644 index 0000000..ce9b42f --- /dev/null +++ b/src/test/java/controller/UserSignupControllerTest.java @@ -0,0 +1,87 @@ +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.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()); + assertEquals("1234", savedUser.getPassword()); + assertEquals("홍길동", savedUser.getName()); + assertEquals("test@example.com", savedUser.getEmail()); + } + + @Test + @DisplayName("POST 요청으로 다른 유효한 사용자 정보를 전송하면 회원가입이 성공해야 한다") + public void signupSuccessWithAnotherPostRequest() throws IOException { + // given + 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()); + assertEquals("5678", savedUser.getPassword()); + assertEquals("김철수", savedUser.getName()); + assertEquals("get@example.com", savedUser.getEmail()); + } +} \ No newline at end of file diff --git a/src/test/java/webserver/RequestMapperTest.java b/src/test/java/webserver/RequestMapperTest.java new file mode 100644 index 0000000..c63c2e5 --- /dev/null +++ b/src/test/java/webserver/RequestMapperTest.java @@ -0,0 +1,91 @@ +package webserver; + +import controller.*; +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 static org.junit.jupiter.api.Assertions.*; + +@DisplayName("RequestMapper 테스트") +class RequestMapperTest { + + private RequestMapper requestMapper; + + @BeforeEach + void setUp() { + requestMapper = new RequestMapper(); + } + + @Test + @DisplayName("루트 경로 요청 시 ForwardController를 반환한다") + void shouldReturnForwardControllerWhenRootPath() { + // given + String path = RequestPath.ROOT.getValue(); + HttpMethod method = HttpMethod.GET; + + // when + Controller controller = requestMapper.getController(path, method); + + // then + assertInstanceOf(ForwardController.class, controller); + } + + @Test + @DisplayName("POST /user/signup 요청 시 UserSignupController를 반환한다") + void shouldReturnUserSignupControllerWhenPostUserSignup() { + // given + String path = RequestPath.USER_SIGNUP.getValue(); + HttpMethod method = HttpMethod.POST; + + // when + Controller controller = requestMapper.getController(path, method); + + // then + assertInstanceOf(UserSignupController.class, controller); + } + + @Test + @DisplayName("POST /user/login 요청 시 UserLoginController를 반환한다") + void shouldReturnUserLoginControllerWhenPostUserLogin() { + // given + String path = RequestPath.USER_LOGIN.getValue(); + HttpMethod method = HttpMethod.POST; + + // when + Controller controller = requestMapper.getController(path, method); + + // then + assertInstanceOf(UserLoginController.class, controller); + } + + @Test + @DisplayName("/user/userList 요청 시 UserListController를 반환한다") + void shouldReturnUserListControllerWhenUserList() { + // given + String path = RequestPath.USER_LIST.getValue(); + HttpMethod method = HttpMethod.GET; + + // when + Controller controller = requestMapper.getController(path, method); + + // then + assertInstanceOf(UserListController.class, controller); + } + + @Test + @DisplayName("알 수 없는 경로 요청 시 ForwardController를 기본으로 반환한다") + void shouldReturnForwardControllerWhenUnknownPath() { + // given + String path = "/unknown/path"; + HttpMethod method = HttpMethod.GET; + + // when + Controller controller = requestMapper.getController(path, method); + + // then + assertInstanceOf(ForwardController.class, controller); + } +} \ No newline at end of file diff --git a/src/test/resources/request/get_request.txt b/src/test/resources/request/get_request.txt new file mode 100644 index 0000000..84a5224 --- /dev/null +++ b/src/test/resources/request/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/request/post_request.txt b/src/test/resources/request/post_request.txt new file mode 100644 index 0000000..021b529 --- /dev/null +++ b/src/test/resources/request/post_request.txt @@ -0,0 +1,7 @@ +POST /user/create HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +Content-Length: 35 +Accept: */* + +userId=foden&password=0801&name=PSG \ 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 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

-
+