diff --git a/src/main/java/me/nickhanson/codeforge/config/EnvConfig.java b/src/main/java/me/nickhanson/codeforge/config/EnvConfig.java
index d8fd39a..1941464 100644
--- a/src/main/java/me/nickhanson/codeforge/config/EnvConfig.java
+++ b/src/main/java/me/nickhanson/codeforge/config/EnvConfig.java
@@ -9,7 +9,6 @@
* The environment is determined by the "APP_ENV" environment variable,
* defaulting to "dev" if not set.
* @author Nick Hanson
- * TODO: Is this still needed, now that spring is gone?
*/
public final class EnvConfig {
diff --git a/src/main/java/me/nickhanson/codeforge/config/LocalConfig.java b/src/main/java/me/nickhanson/codeforge/config/LocalConfig.java
index b3a8951..89c6d0a 100644
--- a/src/main/java/me/nickhanson/codeforge/config/LocalConfig.java
+++ b/src/main/java/me/nickhanson/codeforge/config/LocalConfig.java
@@ -2,18 +2,30 @@
import java.util.Properties;
+/**
+ * Loads local configuration properties from a file.
+ */
public class LocalConfig implements PropertiesLoader {
private static Properties localProps;
+ /**
+ * Loads the local configuration properties from 'test-db.properties'.
+ * @return Properties object containing the loaded properties.
+ */
public static Properties load() {
if (localProps == null) {
LocalConfig loader = new LocalConfig();
- localProps = loader.loadProperties("/local.properties");
+ localProps = loader.loadProperties("/test-db.properties");
}
return localProps;
}
+ /**
+ * Retrieves the value of a specific property by key.
+ * @param key The property key.
+ * @return The property value associated with the key.
+ */
public static String get(String key) {
Properties props = load();
return props.getProperty(key);
diff --git a/src/main/java/me/nickhanson/codeforge/controller/StartupServlet.java b/src/main/java/me/nickhanson/codeforge/controller/StartupServlet.java
index b592574..39843a4 100644
--- a/src/main/java/me/nickhanson/codeforge/controller/StartupServlet.java
+++ b/src/main/java/me/nickhanson/codeforge/controller/StartupServlet.java
@@ -5,6 +5,7 @@
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
+import java.time.Year;
import java.util.Properties;
/**
@@ -24,14 +25,7 @@ public void contextInitialized(ServletContextEvent sce) {
Properties props = loadProperties("/cognito.properties");
sce.getServletContext().setAttribute("cognitoProperties", props);
- }
- /**
- * No operation on context destruction.
- * @param sce the servlet context event
- */
- @Override
- public void contextDestroyed(ServletContextEvent sce) {
- // no-op
+ sce.getServletContext().setAttribute("currentYear", Year.now().getValue());
}
}
diff --git a/src/main/java/me/nickhanson/codeforge/entity/AuthenticatedUser.java b/src/main/java/me/nickhanson/codeforge/entity/AuthenticatedUser.java
index 579dca6..b7d3813 100644
--- a/src/main/java/me/nickhanson/codeforge/entity/AuthenticatedUser.java
+++ b/src/main/java/me/nickhanson/codeforge/entity/AuthenticatedUser.java
@@ -1,71 +1,22 @@
package me.nickhanson.codeforge.entity;
+import lombok.*;
+
/**
* Represents an authenticated user in the system.
- * Contains basic user information such as username, email, and unique identifier (sub).
+ *
+ * Session-scoped authenticated user derived from Cognito JWT claims.
+ * This class is not a persisted entity and is not managed by Hibernate.
+ *
* @author Nick Hanson
- * TODO: needs Lombok implementation
*/
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString
public class AuthenticatedUser {
private String userName;
private String email;
private String sub;
-
- // Default constructor
- public AuthenticatedUser() {}
-
- // Parameterized constructor
- public AuthenticatedUser(String userName, String email, String sub) {
- this.userName = userName;
- this.email = email;
- this.sub = sub;
- }
-
- /**
- * Gets the username of the authenticated user.
- * @return the username
- */
- public String getUserName() {
- return userName;
- }
-
- /**
- * Sets the username of the authenticated user.
- * @param userName the username to set
- */
- public void setUserName(String userName) {
- this.userName = userName;
- }
-
- /**
- * Gets the email of the authenticated user.
- * @return the email
- */
- public String getEmail() {
- return email;
- }
-
- /**
- * Sets the email of the authenticated user.
- * @param email the email to set
- */
- public void setEmail(String email) {
- this.email = email;
- }
-
- /**
- * Gets the unique identifier (sub) of the authenticated user.
- * @return the sub
- */
- public String getSub() {
- return sub;
- }
-
- /**
- * Sets the unique identifier (sub) of the authenticated user.
- * @param sub the sub to set
- */
- public void setSub(String sub) {
- this.sub = sub;
- }
}
\ No newline at end of file
diff --git a/src/main/java/me/nickhanson/codeforge/evaluator/AnswerEvaluation.java b/src/main/java/me/nickhanson/codeforge/evaluator/AnswerEvaluation.java
index 450233c..c62ee5c 100644
--- a/src/main/java/me/nickhanson/codeforge/evaluator/AnswerEvaluation.java
+++ b/src/main/java/me/nickhanson/codeforge/evaluator/AnswerEvaluation.java
@@ -2,14 +2,24 @@
import me.nickhanson.codeforge.entity.Outcome;
-// TODO: Add javadoc
-
+/**
+ * Represents the evaluation result of a submitted answer.
+ * Contains the outcome, feedback, and normalized forms of expected and submitted answers.
+ * @author Nick Hanson
+ */
public class AnswerEvaluation {
private final Outcome outcome;
private final String feedback;
private final String normalizedExpected;
private final String normalizedSubmitted;
+ /**
+ * Constructs an AnswerEvaluation with the specified details.
+ * @param outcome The outcome of the evaluation.
+ * @param feedback Feedback regarding the evaluation.
+ * @param normalizedExpected The normalized expected answer.
+ * @param normalizedSubmitted The normalized submitted answer.
+ */
public AnswerEvaluation(Outcome outcome, String feedback, String normalizedExpected, String normalizedSubmitted) {
this.outcome = outcome;
this.feedback = feedback;
@@ -17,6 +27,10 @@ public AnswerEvaluation(Outcome outcome, String feedback, String normalizedExpec
this.normalizedSubmitted = normalizedSubmitted;
}
+ /**
+ * Gets the outcome of the evaluation.
+ * @return The outcome.
+ */
public Outcome getOutcome() { return outcome; }
public String getFeedback() { return feedback; }
public String getNormalizedExpected() { return normalizedExpected; }
diff --git a/src/main/java/me/nickhanson/codeforge/evaluator/AnswerEvaluator.java b/src/main/java/me/nickhanson/codeforge/evaluator/AnswerEvaluator.java
index 90d5e57..9b5494e 100644
--- a/src/main/java/me/nickhanson/codeforge/evaluator/AnswerEvaluator.java
+++ b/src/main/java/me/nickhanson/codeforge/evaluator/AnswerEvaluator.java
@@ -2,7 +2,18 @@
import me.nickhanson.codeforge.entity.Challenge;
+/**
+ * Interface for evaluating submitted answers against challenges.
+ * @author Nick Hanson
+ */
public interface AnswerEvaluator {
+
+ /**
+ * Evaluates a submitted answer for a given challenge.
+ * @param challenge The challenge to evaluate against.
+ * @param submission The submitted answer.
+ * @return An AnswerEvaluation containing the evaluation results.
+ */
AnswerEvaluation evaluate(Challenge challenge, String submission);
}
diff --git a/src/main/java/me/nickhanson/codeforge/evaluator/BasicEvaluatorService.java b/src/main/java/me/nickhanson/codeforge/evaluator/BasicEvaluatorService.java
index 7556e47..67149e0 100644
--- a/src/main/java/me/nickhanson/codeforge/evaluator/BasicEvaluatorService.java
+++ b/src/main/java/me/nickhanson/codeforge/evaluator/BasicEvaluatorService.java
@@ -3,9 +3,20 @@
import me.nickhanson.codeforge.entity.Challenge;
import me.nickhanson.codeforge.entity.Outcome;
+/**
+ * Basic implementation of the AnswerEvaluator interface.
+ * Compares submitted answers to expected answers with normalization.
+ * @author Nick Hanson
+ */
public class BasicEvaluatorService implements AnswerEvaluator {
private static final int MAX_SOURCE_LENGTH = 10000; // simple guard for MVP
+ /**
+ * Evaluates a submitted answer for a given challenge.
+ * @param challenge The challenge to evaluate against.
+ * @param submission The submitted answer.
+ * @return An AnswerEvaluation containing the evaluation results.
+ */
@Override
public AnswerEvaluation evaluate(Challenge challenge, String submission) {
String expected = challenge.getExpectedAnswer();
diff --git a/src/main/java/me/nickhanson/codeforge/evaluator/Normalizer.java b/src/main/java/me/nickhanson/codeforge/evaluator/Normalizer.java
index f12a970..67608a7 100644
--- a/src/main/java/me/nickhanson/codeforge/evaluator/Normalizer.java
+++ b/src/main/java/me/nickhanson/codeforge/evaluator/Normalizer.java
@@ -2,19 +2,43 @@
import java.util.Locale;
+/**
+ * Utility class for normalizing strings for evaluation.
+ * Provides methods to standardize strings by trimming whitespace,
+ * converting to lowercase, collapsing multiple spaces, and stripping punctuation.
+ * @author Nick Hanson
+ */
public final class Normalizer {
private Normalizer() {}
+ /**
+ * Performs basic normalization on the input string:
+ * - Trims leading and trailing whitespace
+ * - Converts to lowercase
+ * - Collapses multiple whitespace characters into a single space
+ * @param s The input string to normalize
+ * @return The normalized string
+ */
public static String basic(String s) {
if (s == null) return null;
String trimmed = s.trim().toLowerCase(Locale.ROOT);
return collapseWhitespace(trimmed);
}
+ /**
+ * Collapses multiple consecutive whitespace characters into a single space.
+ * @param s The input string
+ * @return The string with collapsed whitespace
+ */
public static String collapseWhitespace(String s) {
return s.replaceAll("\\s+", " ");
}
+ /**
+ * Strips all punctuation characters from the input string.
+ * @param s The input string
+ * @return The string without punctuation
+ */
public static String stripPunctuation(String s) {
if (s == null) return null;
return s.replaceAll("[\\p{Punct}]", "");
diff --git a/src/main/java/me/nickhanson/codeforge/persistence/ChallengeDao.java b/src/main/java/me/nickhanson/codeforge/persistence/ChallengeDao.java
index ef1bebe..99403ba 100644
--- a/src/main/java/me/nickhanson/codeforge/persistence/ChallengeDao.java
+++ b/src/main/java/me/nickhanson/codeforge/persistence/ChallengeDao.java
@@ -6,6 +6,11 @@
import org.apache.logging.log4j.Logger;
import org.hibernate.Session;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+
import java.util.List;
/**
@@ -49,6 +54,7 @@ public List findByDifficulty(Difficulty difficulty) {
return data.findByPropertyEqual("difficulty", difficulty);
}
+
/**
* Checks if a challenge with the given title exists, ignoring case.
* @param title The title to check for existence.
@@ -56,10 +62,16 @@ public List findByDifficulty(Difficulty difficulty) {
*/
public boolean existsTitleIgnoreCase(String title) {
try (Session s = SessionFactoryProvider.getSessionFactory().openSession()) {
- Long count = s.createQuery(
- "select count(c) from Challenge c where lower(c.title) = lower(:t)", Long.class)
- .setParameter("t", title)
- .getSingleResult();
+ CriteriaBuilder cb = s.getCriteriaBuilder();
+ CriteriaQuery cq = cb.createQuery(Long.class);
+ Root root = cq.from(Challenge.class);
+
+ Predicate sameTitleIgnoreCase =
+ cb.equal(cb.lower(root.get("title")), title.toLowerCase());
+
+ cq.select(cb.count(root)).where(sameTitleIgnoreCase);
+
+ Long count = s.createQuery(cq).getSingleResult();
return count != null && count > 0;
}
}
@@ -73,12 +85,18 @@ public boolean existsTitleIgnoreCase(String title) {
*/
public boolean existsTitleForOtherIgnoreCase(String title, Long excludeId) {
try (Session s = SessionFactoryProvider.getSessionFactory().openSession()) {
- Long count = s.createQuery(
- "select count(c) from Challenge c where lower(c.title) = lower(:t) and c.id <> :id",
- Long.class)
- .setParameter("t", title)
- .setParameter("id", excludeId)
- .getSingleResult();
+ CriteriaBuilder cb = s.getCriteriaBuilder();
+ CriteriaQuery cq = cb.createQuery(Long.class);
+ Root root = cq.from(Challenge.class);
+
+ Predicate sameTitleIgnoreCase =
+ cb.equal(cb.lower(root.get("title")), title.toLowerCase());
+ Predicate notSameId =
+ cb.notEqual(root.get("id"), excludeId);
+
+ cq.select(cb.count(root)).where(cb.and(sameTitleIgnoreCase, notSameId));
+
+ Long count = s.createQuery(cq).getSingleResult();
return count != null && count > 0;
}
}
diff --git a/src/main/java/me/nickhanson/codeforge/service/Week9ChallengeService.java b/src/main/java/me/nickhanson/codeforge/service/Week9ChallengeService.java
deleted file mode 100644
index ea71b55..0000000
--- a/src/main/java/me/nickhanson/codeforge/service/Week9ChallengeService.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package me.nickhanson.codeforge.service;
-
-import me.nickhanson.codeforge.entity.Challenge;
-
-import java.util.List;
-import java.util.Optional;
-
-// TODO: Remove this class ASAP as it is not needed anymore.
-
-/**
- * A temporary service class for Week 9 challenges.
- * This class is intended to be removed as soon as possible.
- * It provides methods to find all challenges and find a challenge by its ID.
- * @author Nick Hanson
- */
-public class Week9ChallengeService {
- private final ChallengeService svc;
-
- public Week9ChallengeService() {
- this.svc = new ChallengeService();
- }
-
- public Week9ChallengeService(ChallengeService svc) {
- this.svc = svc;
- }
-
- public List findAll() {
- return svc.listChallenges(null);
- }
-
- public Optional findById(Long id) {
- return svc.getById(id);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/me/nickhanson/codeforge/web/AppBootstrap.java b/src/main/java/me/nickhanson/codeforge/web/AppBootstrap.java
index cfb22a5..bf7f428 100644
--- a/src/main/java/me/nickhanson/codeforge/web/AppBootstrap.java
+++ b/src/main/java/me/nickhanson/codeforge/web/AppBootstrap.java
@@ -69,13 +69,4 @@ public void contextInitialized(ServletContextEvent sce) {
ctx.log("Seeding failed: " + e.getMessage(), e);
}
}
-
- /**
- * No operation on context destruction.
- * @param sce the servlet context event
- */
- @Override
- public void contextDestroyed(ServletContextEvent sce) {
- // no-op
- }
}
diff --git a/src/main/java/me/nickhanson/codeforge/web/ErrorServlet.java b/src/main/java/me/nickhanson/codeforge/web/ErrorServlet.java
new file mode 100644
index 0000000..1d6fc31
--- /dev/null
+++ b/src/main/java/me/nickhanson/codeforge/web/ErrorServlet.java
@@ -0,0 +1,100 @@
+package me.nickhanson.codeforge.web;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * Servlet to handle errors and display appropriate error pages.
+ * Supports handling of 403, 404, and 500 errors with custom JSP views.
+ * Extracts error details from request attributes and forwards to the relevant JSP.
+ * @author Nick Hanson
+ */
+@WebServlet("/error")
+public class ErrorServlet extends HttpServlet {
+
+ /**
+ * Handles GET requests by delegating to the common error handling method.
+ */
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ handle(req, resp);
+ }
+
+ /**
+ * Handles POST requests by delegating to the common error handling method.
+ */
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ handle(req, resp);
+ }
+
+ /**
+ * Common error handling logic for both GET and POST requests.
+ * Extracts error details and forwards to the appropriate JSP view.
+ */
+ private void handle(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ // Extract standard servlet error attributes
+ Integer status = (Integer) req.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
+ Throwable throwable = (Throwable) req.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
+ String requestUri = (String) req.getAttribute(RequestDispatcher.ERROR_REQUEST_URI);
+ String message = (String) req.getAttribute(RequestDispatcher.ERROR_MESSAGE);
+
+ if (status == null) {
+ status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+ }
+ if (requestUri == null || requestUri.isEmpty()) {
+ requestUri = req.getRequestURI();
+ }
+
+ // Provide a friendly, concise errorMessage for the JSP (500.jsp expects this optionally)
+ String errorMessage;
+ if (message != null && !message.isBlank()) {
+ errorMessage = message;
+ } else if (throwable != null) {
+ errorMessage = Objects.toString(throwable.getMessage(), throwable.getClass().getSimpleName());
+ } else {
+ errorMessage = "Unexpected error";
+ }
+
+ // Common extra context useful in views or logs
+ req.setAttribute("timestamp", Instant.now().toString());
+ req.setAttribute("referer", req.getHeader("Referer"));
+
+ // Align with existing JSPs: they read errorMessage and javax.servlet.error.request_uri
+ req.setAttribute("errorMessage", errorMessage);
+ req.setAttribute("javax.servlet.error.request_uri", requestUri);
+ req.setAttribute("javax.servlet.error.status_code", status);
+
+ // Choose view based on status code
+ String view;
+ switch (status) {
+ case HttpServletResponse.SC_FORBIDDEN: // 403
+ view = "/WEB-INF/jsp/error/403.jsp";
+ break;
+ case HttpServletResponse.SC_NOT_FOUND: // 404
+ view = "/WEB-INF/jsp/error/404.jsp";
+ break;
+ case HttpServletResponse.SC_INTERNAL_SERVER_ERROR: // 500
+ case HttpServletResponse.SC_BAD_REQUEST: // 400 -> treat as 500 view for now
+ default:
+ view = "/WEB-INF/jsp/error/500.jsp";
+ break;
+ }
+
+ // Set the response status code
+ resp.setStatus(status);
+
+ // Forward to the chosen error page
+ req.getRequestDispatcher(view).forward(req, resp);
+ }
+}
diff --git a/src/main/java/me/nickhanson/codeforge/web/HomeServlet.java b/src/main/java/me/nickhanson/codeforge/web/HomeServlet.java
index abda303..def4b79 100644
--- a/src/main/java/me/nickhanson/codeforge/web/HomeServlet.java
+++ b/src/main/java/me/nickhanson/codeforge/web/HomeServlet.java
@@ -15,7 +15,7 @@
*
* @author Nick Hanson
*/
-@WebServlet(urlPatterns = {"/home"})
+@WebServlet(urlPatterns = {"/", "/home"})
public class HomeServlet extends HttpServlet {
private final QuoteService quotes = new QuoteService();
@@ -33,5 +33,4 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
req.setAttribute("quote", quote);
req.getRequestDispatcher("/WEB-INF/jsp/home.jsp").forward(req, resp);
}
-}
-
+}
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/about.jsp b/src/main/webapp/WEB-INF/jsp/about.jsp
index 476fd7b..6858fdc 100644
--- a/src/main/webapp/WEB-INF/jsp/about.jsp
+++ b/src/main/webapp/WEB-INF/jsp/about.jsp
@@ -4,23 +4,19 @@
Date: 11/14/2025
Time: 11:52 PM
--%>
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-
-
-
+
-
-
- <%@ include file="/WEB-INF/jsp/head-meta.jspf" %>
+
-
+
-
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/auth/me.jsp b/src/main/webapp/WEB-INF/jsp/auth/me.jsp
index 17b1f00..311a7f8 100644
--- a/src/main/webapp/WEB-INF/jsp/auth/me.jsp
+++ b/src/main/webapp/WEB-INF/jsp/auth/me.jsp
@@ -6,23 +6,17 @@
--%>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-
-
-<%-- Fallback: if "user" isn't on the request, pull it from the session --%>
-
-
-
+
-
-
- <%@ include file="/WEB-INF/jsp/head-meta.jspf" %>
-
+
+
-<%@ include file="/WEB-INF/jsp/header.jsp" %>
+
+
-<%@ include file="/WEB-INF/jsp/footer.jsp" %>
+
+
diff --git a/src/main/webapp/WEB-INF/jsp/challenges/detail.jsp b/src/main/webapp/WEB-INF/jsp/challenges/detail.jsp
index ecc410f..94879e8 100644
--- a/src/main/webapp/WEB-INF/jsp/challenges/detail.jsp
+++ b/src/main/webapp/WEB-INF/jsp/challenges/detail.jsp
@@ -6,19 +6,17 @@
--%>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
-
+
-
-
- <%@ include file="/WEB-INF/jsp/head-meta.jspf" %>
+
-
+
+
-
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/challenges/edit.jsp b/src/main/webapp/WEB-INF/jsp/challenges/edit.jsp
index 51dbea7..74383b4 100644
--- a/src/main/webapp/WEB-INF/jsp/challenges/edit.jsp
+++ b/src/main/webapp/WEB-INF/jsp/challenges/edit.jsp
@@ -4,20 +4,18 @@
Date: 10/24/2025
Time: 2:30 PM
--%>
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-
+
-
-
- <%@ include file="/WEB-INF/jsp/head-meta.jspf" %>
+
-
+
-
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/challenges/list.jsp b/src/main/webapp/WEB-INF/jsp/challenges/list.jsp
index 5af4128..b4feee8 100644
--- a/src/main/webapp/WEB-INF/jsp/challenges/list.jsp
+++ b/src/main/webapp/WEB-INF/jsp/challenges/list.jsp
@@ -6,19 +6,16 @@
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
-
+
-
-
- <%@ include file="/WEB-INF/jsp/head-meta.jspf" %>
+
-
+
-
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/challenges/new.jsp b/src/main/webapp/WEB-INF/jsp/challenges/new.jsp
index a8149de..58137de 100644
--- a/src/main/webapp/WEB-INF/jsp/challenges/new.jsp
+++ b/src/main/webapp/WEB-INF/jsp/challenges/new.jsp
@@ -4,20 +4,18 @@
Date: 10/24/2025
Time: 2:30 PM
--%>
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-
+
-
-
- <%@ include file="/WEB-INF/jsp/head-meta.jspf" %>
+
-
+
-
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/drill/queue.jsp b/src/main/webapp/WEB-INF/jsp/drill/queue.jsp
index 7b36825..b31b4f7 100644
--- a/src/main/webapp/WEB-INF/jsp/drill/queue.jsp
+++ b/src/main/webapp/WEB-INF/jsp/drill/queue.jsp
@@ -7,20 +7,18 @@
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
-
+
-
-
- <%@ include file="/WEB-INF/jsp/head-meta.jspf" %>
+
-
+
+
+
diff --git a/src/main/webapp/WEB-INF/jsp/drill/solve.jsp b/src/main/webapp/WEB-INF/jsp/drill/solve.jsp
index b5963ef..cf6d124 100644
--- a/src/main/webapp/WEB-INF/jsp/drill/solve.jsp
+++ b/src/main/webapp/WEB-INF/jsp/drill/solve.jsp
@@ -7,21 +7,20 @@
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
-
+
-
-
- <%@ include file="/WEB-INF/jsp/head-meta.jspf" %>
+
+
-
+
-
+
-
+
+
+
+
+
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
index b2d67d4..50282d9 100644
--- a/src/main/webapp/WEB-INF/web.xml
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -17,13 +17,19 @@
+
+ 403
+ /error
+
+
404
- /WEB-INF/jsp/error/404.jsp
+ /error
+
500
- /WEB-INF/jsp/error/500.jsp
+ /error
diff --git a/src/test/java/me/nickhanson/codeforge/auth/CognitoJWTParserTest.java b/src/test/java/me/nickhanson/codeforge/auth/CognitoJWTParserTest.java
index 1f8ac6e..9651894 100644
--- a/src/test/java/me/nickhanson/codeforge/auth/CognitoJWTParserTest.java
+++ b/src/test/java/me/nickhanson/codeforge/auth/CognitoJWTParserTest.java
@@ -8,12 +8,18 @@
class CognitoJWTParserTest {
+ /**
+ * Verifies that validateJWT throws an exception on a malformed JWT token.
+ */
@Test
void validateJWT_throwsOnBadToken() {
// CognitoJWTParser.validateJWT throws java.security.InvalidParameterException on malformed JWT
assertThrows(java.security.InvalidParameterException.class, () -> CognitoJWTParser.validateJWT("notajwt"));
}
+ /**
+ * Verifies that getPayload and getClaim return the correct claim values from a JWT token.
+ */
@Test
void getPayload_and_getClaim_returnsClaimValue() {
// create a simple jwt-like string: header.payload.signature where payload has {"sub":"123","email":"a@b"}
diff --git a/src/test/java/me/nickhanson/codeforge/auth/TokenResponseMappingTest.java b/src/test/java/me/nickhanson/codeforge/auth/TokenResponseMappingTest.java
index 9a8aee6..be8c8c8 100644
--- a/src/test/java/me/nickhanson/codeforge/auth/TokenResponseMappingTest.java
+++ b/src/test/java/me/nickhanson/codeforge/auth/TokenResponseMappingTest.java
@@ -7,6 +7,10 @@
class TokenResponseMappingTest {
+ /**
+ * Verifies that Jackson correctly maps a JSON string to a TokenResponse object.
+ * @throws Exception if mapping fails
+ */
@Test
void jackson_maps_json_to_tokenResponse() throws Exception {
String json = "{\"access_token\":\"a\",\"refresh_token\":\"r\",\"id_token\":\"i\",\"token_type\":\"t\",\"expires_in\":3600}";
diff --git a/src/test/java/me/nickhanson/codeforge/controller/AuthTest.java b/src/test/java/me/nickhanson/codeforge/controller/AuthTest.java
index 14e772a..1765f05 100644
--- a/src/test/java/me/nickhanson/codeforge/controller/AuthTest.java
+++ b/src/test/java/me/nickhanson/codeforge/controller/AuthTest.java
@@ -25,6 +25,10 @@ class AuthTest {
@Mock HttpServletResponse resp;
@Mock RequestDispatcher rd;
+ /**
+ * Sets up the Auth servlet and provides a mock ServletConfig before each test.
+ * Initializes the servlet while ignoring ServletException during initialization.
+ */
@BeforeEach
void setup() throws Exception {
servlet = new Auth();
@@ -45,6 +49,9 @@ void setup() throws Exception {
}
}
+ /**
+ * Verifies that doGet with a valid code parameter forwards to the callback JSP.
+ */
@Test
void doGet_missingCode_forwardsToError() throws Exception {
when(req.getParameter("code")).thenReturn(null);
diff --git a/src/test/java/me/nickhanson/codeforge/controller/LogInTest.java b/src/test/java/me/nickhanson/codeforge/controller/LogInTest.java
index 283c8d1..a4a5fcb 100644
--- a/src/test/java/me/nickhanson/codeforge/controller/LogInTest.java
+++ b/src/test/java/me/nickhanson/codeforge/controller/LogInTest.java
@@ -31,6 +31,10 @@ class LogInTest {
ServletConfig config;
+ /**
+ * Sets up the LogIn servlet and provides a mock ServletConfig before each test.
+ * Initializes the servlet.
+ */
@BeforeEach
void setup() throws Exception {
lenient().when(req.getServletContext()).thenReturn(ctx);
@@ -47,6 +51,9 @@ void setup() throws Exception {
servlet.init(config);
}
+ /**
+ * Verifies that doGet redirects to login when Cognito properties are present.
+ */
@Test
void doGet_redirectsToLogin_whenConfigPresent() throws Exception {
servlet.doGet(req, resp);
diff --git a/src/test/java/me/nickhanson/codeforge/controller/LogOutTest.java b/src/test/java/me/nickhanson/codeforge/controller/LogOutTest.java
index 0b3c712..00578b7 100644
--- a/src/test/java/me/nickhanson/codeforge/controller/LogOutTest.java
+++ b/src/test/java/me/nickhanson/codeforge/controller/LogOutTest.java
@@ -31,6 +31,10 @@ class LogOutTest {
ServletConfig config;
+ /**
+ * Sets up the LogOut servlet and provides a mock ServletConfig before each test.
+ * Initializes the servlet.
+ */
@BeforeEach
void setup() throws Exception {
lenient().when(req.getServletContext()).thenReturn(ctx);
@@ -46,6 +50,9 @@ void setup() throws Exception {
servlet.init();
}
+ /**
+ * Verifies that doGet invalidates the session and redirects.
+ */
@Test
void doGet_invalidatesSession_andRedirects() throws Exception {
servlet.doGet(req, resp);
diff --git a/src/test/java/me/nickhanson/codeforge/controller/StartupServletTest.java b/src/test/java/me/nickhanson/codeforge/controller/StartupServletTest.java
index cef70c8..2b1e29b 100644
--- a/src/test/java/me/nickhanson/codeforge/controller/StartupServletTest.java
+++ b/src/test/java/me/nickhanson/codeforge/controller/StartupServletTest.java
@@ -9,6 +9,9 @@
class StartupServletTest {
+ /**
+ * Verifies that contextInitialized sets the "cognitoProperties" attribute in the ServletContext.
+ */
@Test
void contextInitialized_setsCognitoPropertiesAttribute() {
ServletContext ctx = mock(ServletContext.class);
diff --git a/src/test/java/me/nickhanson/codeforge/evaluator/BasicEvaluatorServiceTest.java b/src/test/java/me/nickhanson/codeforge/evaluator/BasicEvaluatorServiceTest.java
index 13fa5c8..0c421f4 100644
--- a/src/test/java/me/nickhanson/codeforge/evaluator/BasicEvaluatorServiceTest.java
+++ b/src/test/java/me/nickhanson/codeforge/evaluator/BasicEvaluatorServiceTest.java
@@ -10,12 +10,18 @@ public class BasicEvaluatorServiceTest {
private final BasicEvaluatorService evaluator = new BasicEvaluatorService();
+ /**
+ * Helper method to create a Challenge with a specified expected answer.
+ */
private Challenge challengeWithExpected(String expected) {
Challenge ch = new Challenge("Test", Difficulty.EASY, "", "");
ch.setExpectedAnswer(expected);
return ch;
}
+ /**
+ * Verifies that an exact match returns CORRECT outcome.
+ */
@Test
void exactMatch_isCorrect() {
Challenge ch = challengeWithExpected("pivotIndex");
@@ -23,6 +29,9 @@ void exactMatch_isCorrect() {
assertEquals(Outcome.CORRECT, eval.getOutcome());
}
+ /**
+ * Verifies that a match ignoring punctuation and spacing returns ACCEPTABLE outcome.
+ */
@Test
void punctuationOrSpacing_only_isAcceptable() {
Challenge ch = challengeWithExpected("pivot index");
@@ -30,6 +39,9 @@ void punctuationOrSpacing_only_isAcceptable() {
assertEquals(Outcome.ACCEPTABLE, eval.getOutcome());
}
+ /**
+ * Verifies that a mismatched answer returns INCORRECT outcome.
+ */
@Test
void mismatch_isIncorrect() {
Challenge ch = challengeWithExpected("pivotIndex");
@@ -37,6 +49,9 @@ void mismatch_isIncorrect() {
assertEquals(Outcome.INCORRECT, eval.getOutcome());
}
+ /**
+ * Verifies that an empty expected answer results in INCORRECT outcome.
+ */
@Test
void missingExpected_marksIncorrect() {
Challenge ch = challengeWithExpected("");
@@ -44,6 +59,9 @@ void missingExpected_marksIncorrect() {
assertEquals(Outcome.INCORRECT, eval.getOutcome());
}
+ /**
+ * Verifies that a submission exceeding the length limit is SKIPPED by the guard.
+ */
@Test
void longSubmission_isSkippedByGuard() {
Challenge ch = challengeWithExpected("pivotIndex");
diff --git a/src/test/java/me/nickhanson/codeforge/persistence/ChallengeDaoTest.java b/src/test/java/me/nickhanson/codeforge/persistence/ChallengeDaoTest.java
index 5b822d1..e46793c 100644
--- a/src/test/java/me/nickhanson/codeforge/persistence/ChallengeDaoTest.java
+++ b/src/test/java/me/nickhanson/codeforge/persistence/ChallengeDaoTest.java
@@ -18,6 +18,9 @@ class ChallengeDaoTest extends DbReset {
private final DrillItemDao drillDao = new DrillItemDao();
private final SubmissionDao submissionDao = new SubmissionDao();
+ /**
+ * Verifies that saving a new Challenge assigns it an ID.
+ */
@Test
void create_assignsId() {
Challenge c1 = new Challenge("Unique Alpha", Difficulty.EASY, "Find two numbers sum to target", "...");
@@ -25,6 +28,9 @@ void create_assignsId() {
assertNotNull(c1.getId());
}
+ /**
+ * Verifies that retrieving a Challenge by ID returns the saved entity.
+ */
@Test
void read_getById_returnsSavedEntity() {
Challenge c1 = new Challenge("Unique Beta", Difficulty.EASY, "Find two numbers sum to target", "...");
@@ -35,6 +41,9 @@ void read_getById_returnsSavedEntity() {
assertEquals(Difficulty.EASY, found.getDifficulty());
}
+ /**
+ * Verifies that updating a Challenge persists the changes.
+ */
@Test
void update_persistsChanges() {
Challenge c1 = new Challenge("Unique Gamma", Difficulty.EASY, "Find two numbers sum to target", "...");
@@ -45,6 +54,9 @@ void update_persistsChanges() {
assertEquals("Unique Gamma v2", updated.getTitle());
}
+ /**
+ * Verifies that getAll returns all saved Challenges.
+ */
@Test
void getAll_returnsAll() {
int before = dao.getAll().size();
@@ -54,12 +66,18 @@ void getAll_returnsAll() {
assertEquals(before + 2, all.size());
}
+ /**
+ * Verifies that existsTitleIgnoreCase returns false for non-existent title.
+ */
@Test
void existsTitleIgnoreCase_trueAfterInsert() {
dao.saveOrUpdate(new Challenge("Unique Zeta", Difficulty.EASY, "", "..."));
assertTrue(dao.existsTitleIgnoreCase("unique zeta"));
}
+ /**
+ * Verifies that existsTitleForOtherIgnoreCase returns false when excluding the same entity.
+ */
@Test
void existsTitleForOtherIgnoreCase_falseForSameEntity() {
Challenge c1 = new Challenge("Unique Eta", Difficulty.EASY, "", "...");
@@ -67,6 +85,9 @@ void existsTitleForOtherIgnoreCase_falseForSameEntity() {
assertFalse(dao.existsTitleForOtherIgnoreCase("unique eta", c1.getId()));
}
+ /**
+ * Verifies that findByDifficulty returns only challenges matching the specified difficulty.
+ */
@Test
void findByDifficulty_returnsOnlyMatching() {
int beforeEasy = dao.findByDifficulty(Difficulty.EASY).size();
@@ -77,6 +98,9 @@ void findByDifficulty_returnsOnlyMatching() {
assertEquals(beforeMedium + 1, dao.findByDifficulty(Difficulty.MEDIUM).size());
}
+ /**
+ * Verifies that deleting a Challenge removes it from the database.
+ */
@Test
void delete_removesRow() {
int before = dao.getAll().size();
@@ -89,6 +113,9 @@ void delete_removesRow() {
assertEquals(before, afterDelete);
}
+ /**
+ * Verifies that deleting a Challenge cascades to dependent DrillItems and Submissions.
+ */
@Test
void delete_withDependents_cascadesToChildren() {
Challenge ch = new Challenge("Dependent Test", Difficulty.EASY, "", "...");
diff --git a/src/test/java/me/nickhanson/codeforge/persistence/DaoTestBase.java b/src/test/java/me/nickhanson/codeforge/persistence/DaoTestBase.java
deleted file mode 100644
index 7c0e724..0000000
--- a/src/test/java/me/nickhanson/codeforge/persistence/DaoTestBase.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package me.nickhanson.codeforge.persistence;
-
-import org.hibernate.Session;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-
-import java.sql.Statement;
-
-/**
- * Base class for DAO integration tests. Resets MySQL tables between tests for isolation.
- * MySQL-only for MVP (no H2).
- */
-public abstract class DaoTestBase {
-
- @BeforeEach
- void resetDatabase() {
- // Delete in FK-safe way for MySQL
- try (Session session = SessionFactoryProvider.getSessionFactory().openSession()) {
- session.doWork(conn -> {
- try (Statement st = conn.createStatement()) {
- st.execute("SET FOREIGN_KEY_CHECKS = 0");
- // Use physical table names as created by Hibernate on MySQL
- st.execute("TRUNCATE TABLE SUBMISSIONS");
- st.execute("TRUNCATE TABLE drill_items");
- st.execute("TRUNCATE TABLE CHALLENGES");
- st.execute("SET FOREIGN_KEY_CHECKS = 1");
- }
- });
- }
- }
-
- @AfterEach
- void cleanUp() {
- // No-op for now; tables are already clean. Hook reserved for future use.
- }
-}
\ No newline at end of file
diff --git a/src/test/java/me/nickhanson/codeforge/persistence/DrillItemDaoTest.java b/src/test/java/me/nickhanson/codeforge/persistence/DrillItemDaoTest.java
index 543cbf5..11df460 100644
--- a/src/test/java/me/nickhanson/codeforge/persistence/DrillItemDaoTest.java
+++ b/src/test/java/me/nickhanson/codeforge/persistence/DrillItemDaoTest.java
@@ -16,12 +16,18 @@ class DrillItemDaoTest extends DbReset {
private static final String USER = "unit-test-user";
+ /**
+ * Helper method to create and persist a Challenge with the given title.
+ */
private Challenge seedChallenge(String title) {
Challenge ch = new Challenge(title, Difficulty.EASY, "", "...");
challengeDao.saveOrUpdate(ch);
return ch;
}
+ /**
+ * Verifies that creating a DrillItem assigns it an ID and relates it to the Challenge.
+ */
@Test
void create_assignsId_and_relatesToChallenge() {
Challenge ch = seedChallenge("FizzBuzz");
@@ -32,6 +38,9 @@ void create_assignsId_and_relatesToChallenge() {
assertEquals(ch.getId(), di.getChallenge().getId());
}
+ /**
+ * Verifies that listing DrillItems by Challenge ID returns the correct items.
+ */
@Test
void listByChallenge_returnsItems() {
Challenge ch = seedChallenge("FizzBuzz");
@@ -43,6 +52,9 @@ void listByChallenge_returnsItems() {
assertEquals(ch.getId(), items.get(0).getChallenge().getId());
}
+ /**
+ * Verifies that dueQueue orders items with null nextDueAt first, then by time.
+ */
@Test
void dueQueue_ordersNullFirst_thenByTime() {
// Ensure we control the data set
@@ -86,6 +98,9 @@ void dueQueue_ordersNullFirst_thenByTime() {
assertFalse(challengeIds.contains(chFuture.getId()));
}
+ /**
+ * Verifies that deleting a DrillItem does not delete the associated Challenge.
+ */
@Test
void delete_item_keepsChallenge() {
int beforeChallengeCount = challengeDao.getAll().size();
@@ -102,6 +117,9 @@ void delete_item_keepsChallenge() {
assertEquals(beforeItemCount, drillDao.getAll().size());
}
+ /**
+ * Verifies that deleting a Challenge with dependent DrillItems requires cleaning up the DrillItems first.
+ */
@Test
void delete_challenge_requiresCleaningDependents() {
int beforeChallengeCount = challengeDao.getAll().size();
diff --git a/src/test/java/me/nickhanson/codeforge/persistence/SubmissionDaoTest.java b/src/test/java/me/nickhanson/codeforge/persistence/SubmissionDaoTest.java
index 0b91138..e9a464f 100644
--- a/src/test/java/me/nickhanson/codeforge/persistence/SubmissionDaoTest.java
+++ b/src/test/java/me/nickhanson/codeforge/persistence/SubmissionDaoTest.java
@@ -16,12 +16,18 @@ class SubmissionDaoTest extends DbReset {
private static final String USER = "unit-test-user";
+ /**
+ * Helper method to create and persist a Challenge with the given title.
+ */
private Challenge seedChallenge(String title) {
Challenge ch = new Challenge(title, Difficulty.EASY, "", "...");
challengeDao.saveOrUpdate(ch);
return ch;
}
+ /**
+ * Verifies that creating a Submission assigns it an ID.
+ */
@Test
void create_assignsId() {
Challenge ch = seedChallenge("Reverse String");
@@ -31,6 +37,9 @@ void create_assignsId() {
assertNotNull(s.getId());
}
+ /**
+ * Verifies that retrieving a Submission by ID returns the saved entity.
+ */
@Test
void read_getById_returnsSaved() {
Challenge ch = seedChallenge("Reverse String");
@@ -43,6 +52,9 @@ void read_getById_returnsSaved() {
assertEquals(ch.getId(), found.getChallenge().getId());
}
+ /**
+ * Verifies that listing Submissions by Challenge ID returns only matching Submissions.
+ */
@Test
void listByChallenge_returnsOnlyMatching() {
Challenge ch = seedChallenge("Reverse String");
@@ -58,6 +70,9 @@ void listByChallenge_returnsOnlyMatching() {
assertEquals(ch.getId(), list.get(0).getChallenge().getId());
}
+ /**
+ * Verifies that deleting a Submission does not delete its associated Challenge.
+ */
@Test
void delete_submission_keepsChallenge() {
int beforeChallengeCount = challengeDao.getAll().size();
@@ -73,6 +88,9 @@ void delete_submission_keepsChallenge() {
assertEquals(beforeSubmissionCount, submissionDao.getAll().size());
}
+ /**
+ * Verifies that deleting a Challenge requires cleaning up dependent DrillItems and Submissions first.
+ */
@Test
void delete_challenge_requiresCleaningDependents() {
int beforeChallengeCount = challengeDao.getAll().size();
diff --git a/src/test/java/me/nickhanson/codeforge/service/ChallengeRunServiceTest.java b/src/test/java/me/nickhanson/codeforge/service/ChallengeRunServiceTest.java
index c168594..c78e1d6 100644
--- a/src/test/java/me/nickhanson/codeforge/service/ChallengeRunServiceTest.java
+++ b/src/test/java/me/nickhanson/codeforge/service/ChallengeRunServiceTest.java
@@ -9,6 +9,9 @@ class ChallengeRunServiceTest {
private final ChallengeRunService svc = new ChallengeRunService();
+ /**
+ * Verifies that running with unsupported language or blank language results in SKIPPED outcome.
+ */
@Test
void run_skippedForUnsupportedLanguageOrBlank() {
assertEquals(Outcome.SKIPPED, svc.run(1L, null, "code").getOutcome());
diff --git a/src/test/java/me/nickhanson/codeforge/service/ChallengeServiceTest.java b/src/test/java/me/nickhanson/codeforge/service/ChallengeServiceTest.java
index d313278..ba21fdb 100644
--- a/src/test/java/me/nickhanson/codeforge/service/ChallengeServiceTest.java
+++ b/src/test/java/me/nickhanson/codeforge/service/ChallengeServiceTest.java
@@ -24,6 +24,9 @@ class ChallengeServiceTest extends DbReset {
@Mock ChallengeDao dao;
@InjectMocks ChallengeService svc;
+ /**
+ * Verifies that listing all challenges returns what the DAO provides.
+ */
@Test
void findAll() {
List all = List.of(
@@ -39,6 +42,9 @@ void findAll() {
verifyNoMoreInteractions(dao);
}
+ /**
+ * Verifies that listing challenges by difficulty returns what the DAO provides.
+ */
@Test
void findById_returnsOne_whenPresent() {
Challenge c = new Challenge("Two Sum", Difficulty.EASY, "b","p");
@@ -49,6 +55,9 @@ void findById_returnsOne_whenPresent() {
verify(dao, times(2)).getById(42L); // or call once then store the Optional
}
+ /**
+ * Verifies that creating a challenge sets fields from the form and saves it.
+ */
@Test
void create_setsFields_and_saves() {
ChallengeForm form = new ChallengeForm();
diff --git a/src/test/java/me/nickhanson/codeforge/service/DrillServiceEnrollmentTest.java b/src/test/java/me/nickhanson/codeforge/service/DrillServiceEnrollmentTest.java
index 432d53b..b3a9bc6 100644
--- a/src/test/java/me/nickhanson/codeforge/service/DrillServiceEnrollmentTest.java
+++ b/src/test/java/me/nickhanson/codeforge/service/DrillServiceEnrollmentTest.java
@@ -27,6 +27,10 @@ class DrillServiceEnrollmentTest {
@InjectMocks DrillService service;
+ /**
+ * Ensures that when enrolling a user in challenges, only missing DrillItems are created.
+ * @throws Exception if reflection fails
+ */
@Test
void ensureEnrollmentForUser_createsOnlyMissing() throws Exception {
String userId = "enroll-user";
diff --git a/src/test/java/me/nickhanson/codeforge/service/DrillServicePersistenceTest.java b/src/test/java/me/nickhanson/codeforge/service/DrillServicePersistenceTest.java
index 668fb76..7089a29 100644
--- a/src/test/java/me/nickhanson/codeforge/service/DrillServicePersistenceTest.java
+++ b/src/test/java/me/nickhanson/codeforge/service/DrillServicePersistenceTest.java
@@ -27,6 +27,9 @@ void setup() {
drillItemDao = new DrillItemDao();
}
+ /**
+ * Tests that recording an outcome updates the DrillItem fields appropriately.
+ */
@Test
void recordOutcome_updatesDrillItemFields() {
// Seed a challenge
diff --git a/src/test/java/me/nickhanson/codeforge/service/DrillServiceTest.java b/src/test/java/me/nickhanson/codeforge/service/DrillServiceTest.java
index 727dae7..af808f1 100644
--- a/src/test/java/me/nickhanson/codeforge/service/DrillServiceTest.java
+++ b/src/test/java/me/nickhanson/codeforge/service/DrillServiceTest.java
@@ -27,12 +27,19 @@ class DrillServiceTest {
@InjectMocks DrillService service;
+ /**
+ * Utility to create a Challenge with a specific ID via reflection.
+ */
private static Challenge challengeWithId(long id) {
Challenge c = new Challenge("Two Sum", Difficulty.EASY, "b", "p");
try { var f = Challenge.class.getDeclaredField("id"); f.setAccessible(true); f.set(c, id); } catch (Exception ignored) {}
return c;
}
+ /**
+ * Tests that recording an outcome creates a Submission,
+ * updates the corresponding DrillItem, and persists both.
+ */
@Test
void recordOutcome_createsSubmission_updatesDrillItem_andPersistsBoth() {
long id = 42L;
@@ -52,6 +59,9 @@ void recordOutcome_createsSubmission_updatesDrillItem_andPersistsBoth() {
assertEquals(Outcome.CORRECT, s.getOutcome());
}
+ /**
+ * Tests that the nextDueAt is computed correctly based on the current streak.
+ */
@Test
void computeNextDueAt_advancesAccordingToStreak() {
long id = 7L;
@@ -74,6 +84,9 @@ void computeNextDueAt_advancesAccordingToStreak() {
verify(drillDao, atLeast(2)).saveOrUpdate(any());
}
+ /**
+ * Tests that getDueQueue returns due items, or the soonest upcoming if none are due.
+ */
@Test
void getDueQueue_returnsSoonestWhenNoneDue() {
String userId = "demo-user";
@@ -87,6 +100,9 @@ void getDueQueue_returnsSoonestWhenNoneDue() {
assertSame(soonest, queue.get(0));
}
+ /**
+ * Tests that ensureDrillItem creates a DrillItem if missing.
+ */
@Test
void ensureDrillItem_createsWhenMissing() {
long id = 9L;
diff --git a/src/test/java/me/nickhanson/codeforge/service/QuoteServiceTest.java b/src/test/java/me/nickhanson/codeforge/service/QuoteServiceTest.java
index 37e2ac9..ac91ab5 100644
--- a/src/test/java/me/nickhanson/codeforge/service/QuoteServiceTest.java
+++ b/src/test/java/me/nickhanson/codeforge/service/QuoteServiceTest.java
@@ -1,6 +1,5 @@
package me.nickhanson.codeforge.service;
-import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -24,6 +23,9 @@ class QuoteServiceTest {
private QuoteService service;
+ /**
+ * Setup before each test: create QuoteService and inject mocked HttpClient.
+ */
@BeforeEach
void setup() throws Exception {
service = new QuoteService();
diff --git a/src/test/java/me/nickhanson/codeforge/service/Week9ChallengeServiceTest.java b/src/test/java/me/nickhanson/codeforge/service/Week9ChallengeServiceTest.java
deleted file mode 100644
index ab21c3e..0000000
--- a/src/test/java/me/nickhanson/codeforge/service/Week9ChallengeServiceTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package me.nickhanson.codeforge.service;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.*;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-
-import static org.mockito.Mockito.verify;
-
-@ExtendWith(MockitoExtension.class)
-public class Week9ChallengeServiceTest {
-
- @Mock ChallengeService svc;
- Week9ChallengeService w9;
-
- @BeforeEach
- void setUp() { w9 = new Week9ChallengeService(svc); }
-
- @Test
- void findAll() {
- w9.findAll();
- verify(svc).listChallenges(null);
- }
-
- @Test
- void findById() {
- Long id = 42L;
- w9.findById(id);
- verify(svc).getById(id);
- }
-}
diff --git a/src/test/java/me/nickhanson/codeforge/testutil/Database.java b/src/test/java/me/nickhanson/codeforge/testutil/Database.java
index 425b1d6..0cb0220 100644
--- a/src/test/java/me/nickhanson/codeforge/testutil/Database.java
+++ b/src/test/java/me/nickhanson/codeforge/testutil/Database.java
@@ -30,7 +30,7 @@ public class Database implements PropertiesLoader {
/** private constructor prevents instantiating this class anywhere else */
private Database() {
- properties = loadProperties("/hibernate.properties");
+ properties = loadProperties("/test-db.properties");
}
/** get the single database object */
diff --git a/src/test/java/me/nickhanson/codeforge/testutil/DbReset.java b/src/test/java/me/nickhanson/codeforge/testutil/DbReset.java
index 87f028e..6ed067e 100644
--- a/src/test/java/me/nickhanson/codeforge/testutil/DbReset.java
+++ b/src/test/java/me/nickhanson/codeforge/testutil/DbReset.java
@@ -10,13 +10,7 @@
public abstract class DbReset {
@BeforeEach
public void setUp() {
- try {
- TestDbCleaner.purgeCoreTables();
- } catch (Exception e) {
- // Fallback to full SQL reset if purge fails
- Database database = Database.getInstance();
- database.runSQL("cleandb.sql");
- }
+ Database.getInstance().runSQL("cleandb.sql");
}
/**
diff --git a/src/test/java/me/nickhanson/codeforge/testutil/TestDbCleaner.java b/src/test/java/me/nickhanson/codeforge/testutil/TestDbCleaner.java
deleted file mode 100644
index 49054f3..0000000
--- a/src/test/java/me/nickhanson/codeforge/testutil/TestDbCleaner.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package me.nickhanson.codeforge.testutil;
-
-import me.nickhanson.codeforge.persistence.SessionFactoryProvider;
-import org.hibernate.Session;
-import org.hibernate.Transaction;
-
-/**
- * Utility to purge core tables before tests to avoid unique constraint collisions
- * when using a persistent MySQL test database.
- */
-public final class TestDbCleaner {
- private TestDbCleaner() {}
-
- public static void purgeCoreTables() {
- try (Session session = SessionFactoryProvider.getSessionFactory().openSession()) {
- Transaction tx = session.beginTransaction();
- // Order matters due to FKs
- session.createNativeQuery("DELETE FROM submissions").executeUpdate();
- session.createNativeQuery("DELETE FROM drill_items").executeUpdate();
- session.createNativeQuery("DELETE FROM challenges").executeUpdate();
- tx.commit();
- }
- }
-}
-
diff --git a/src/test/java/me/nickhanson/codeforge/web/ChallengesServletTest.java b/src/test/java/me/nickhanson/codeforge/web/ChallengesServletTest.java
index 166e321..3f67346 100644
--- a/src/test/java/me/nickhanson/codeforge/web/ChallengesServletTest.java
+++ b/src/test/java/me/nickhanson/codeforge/web/ChallengesServletTest.java
@@ -37,6 +37,9 @@ class ChallengesServletTest {
ServletConfig config;
+ /**
+ * Setup before each test: initialize servlet with mocked context and service.
+ */
@BeforeEach
void setup() throws Exception {
lenient().when(req.getServletContext()).thenReturn(ctx);
@@ -53,6 +56,9 @@ void setup() throws Exception {
servlet.init(config);
}
+ /**
+ * Tests that a GET to the challenges list forwards to the list JSP with challenges.
+ */
@Test
void get_list_forwardsToListJsp_withChallenges() throws Exception {
when(req.getPathInfo()).thenReturn(null);
@@ -68,6 +74,9 @@ void get_list_forwardsToListJsp_withChallenges() throws Exception {
verify(rd).forward(req, resp);
}
+ /**
+ * Tests that a GET to a challenge detail forwards to the detail JSP with the challenge.
+ */
@Test
void get_detail_nonexistent_returns404() throws Exception {
when(req.getPathInfo()).thenReturn("/999");
@@ -78,6 +87,9 @@ void get_detail_nonexistent_returns404() throws Exception {
verify(resp).sendError(404);
}
+ /**
+ * Tests that a POST to create a challenge redirects to the new challenge on success.
+ */
@Test
void post_create_redirectsOnSuccess() throws Exception {
when(req.getPathInfo()).thenReturn(null);
@@ -95,6 +107,9 @@ void post_create_redirectsOnSuccess() throws Exception {
verify(resp).sendRedirect(contains("/challenges/123"));
}
+ /**
+ * Tests that a POST to update a challenge redirects to the challenge on success.
+ */
@Test
void post_update_malformedPath_returns400() throws Exception {
// choose a path that parses to a valid id but no known action -> final sendError(400)
@@ -105,6 +120,9 @@ void post_update_malformedPath_returns400() throws Exception {
verify(resp).sendError(400);
}
+ /**
+ * Tests that a POST to delete a challenge redirects to the challenges list on success.
+ */
@Test
void get_detail_invalidId_returns400() throws Exception {
when(req.getPathInfo()).thenReturn("/abc");
@@ -112,6 +130,9 @@ void get_detail_invalidId_returns400() throws Exception {
verify(resp).sendError(400);
}
+ /**
+ * Tests that a GET to detail with an invalid id returns 400.
+ */
@Test
void post_update_invalidId_returns400() throws Exception {
when(req.getPathInfo()).thenReturn("/xyz");
@@ -119,6 +140,9 @@ void post_update_invalidId_returns400() throws Exception {
verify(resp).sendError(400);
}
+ /**
+ * Tests that a POST to delete with an invalid id returns 400.
+ */
@Test
void post_delete_invalidId_returns400() throws Exception {
when(req.getPathInfo()).thenReturn("/xyz/delete");
diff --git a/src/test/java/me/nickhanson/codeforge/web/DrillServletTest.java b/src/test/java/me/nickhanson/codeforge/web/DrillServletTest.java
index 14b091b..5897550 100644
--- a/src/test/java/me/nickhanson/codeforge/web/DrillServletTest.java
+++ b/src/test/java/me/nickhanson/codeforge/web/DrillServletTest.java
@@ -44,6 +44,9 @@ class DrillServletTest {
ServletConfig config;
+ /**
+ * Setup before each test: initialize servlet with mocked context and services.
+ */
@BeforeEach
void setup() throws Exception {
lenient().when(req.getServletContext()).thenReturn(ctx);
@@ -66,6 +69,9 @@ void setup() throws Exception {
servlet.init(config);
}
+ /**
+ * Tests that a GET to the drill endpoint forwards with due drill items.
+ */
@Test
void get_queue_forwardsWithRows() throws Exception {
when(req.getPathInfo()).thenReturn(null);
@@ -78,6 +84,9 @@ void get_queue_forwardsWithRows() throws Exception {
verify(rd).forward(req, resp);
}
+ /**
+ * Tests that a POST to submit code records the outcome and redirects.
+ */
@Test
void post_submit_recordsOutcome_andRedirects() throws Exception {
when(req.getPathInfo()).thenReturn("/42/submit");
@@ -92,6 +101,9 @@ void post_submit_recordsOutcome_andRedirects() throws Exception {
verify(resp).sendRedirect(contains("/drill/next"));
}
+ /**
+ * Tests that a POST to add a challenge to the drill creates the DrillItem and redirects.
+ */
@Test
void get_invalidId_returns400() throws Exception {
when(req.getPathInfo()).thenReturn("/abc");
@@ -99,6 +111,9 @@ void get_invalidId_returns400() throws Exception {
verify(resp).sendError(400);
}
+ /**
+ * Tests that a POST to submit with an invalid ID returns 400.
+ */
@Test
void post_submit_invalidId_returns400() throws Exception {
when(req.getPathInfo()).thenReturn("/xyz/submit");
@@ -106,6 +121,9 @@ void post_submit_invalidId_returns400() throws Exception {
verify(resp).sendError(400);
}
+ /**
+ * Tests that a POST to add with an invalid ID returns 400.
+ */
@Test
void post_add_invalidId_returns400() throws Exception {
when(req.getPathInfo()).thenReturn("/bad/add");
diff --git a/src/test/java/me/nickhanson/codeforge/web/HomeServletTest.java b/src/test/java/me/nickhanson/codeforge/web/HomeServletTest.java
index 02b5560..84fba0a 100644
--- a/src/test/java/me/nickhanson/codeforge/web/HomeServletTest.java
+++ b/src/test/java/me/nickhanson/codeforge/web/HomeServletTest.java
@@ -32,6 +32,9 @@ class HomeServletTest {
ServletConfig config;
+ /**
+ * Setup before each test: initialize servlet with mocked context and service.
+ */
@BeforeEach
void setup() throws Exception {
lenient().when(req.getRequestDispatcher(any())).thenReturn(rd);
@@ -51,6 +54,9 @@ void setup() throws Exception {
servlet.init(config);
}
+ /**
+ * Tests that a GET to the home servlet forwards to the home JSP and sets a quote.
+ */
@Test
void get_forwards_andSetsQuote() throws Exception {
when(quoteService.getRandomQuote()).thenReturn("hello");