diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 84c1d81..bd64a78 100644 --- a/pom.xml +++ b/pom.xml @@ -15,28 +15,97 @@ UTF-8 - 8 - 8 + 23 + 23 - - junit - junit - 4.13.1 + org.junit.jupiter + junit-jupiter-engine + 5.12.1 test - + + org.junit.jupiter + junit-jupiter-params + 5.11.4 + test + + + + org.mockito + mockito-junit-jupiter + 5.15.2 + test + + + + org.mockito + mockito-core + 5.15.2 + test + + + + org.mockito + mockito-inline + 5.2.0 + test + + + jakarta.servlet + jakarta.servlet-api + 6.1.0 + provided + + + jakarta.servlet.jsp.jstl + jakarta.servlet.jsp.jstl-api + 3.0.2 + + + org.glassfish.web + jakarta.servlet.jsp.jstl + 3.0.1 + + + com.fasterxml.jackson.core + jackson-databind + 2.18.3 + + + com.fasterxml.jackson.core + jackson-core + 2.18.3 + + + com.fasterxml.jackson.core + jackson-annotations + 2.18.3 + + + org.slf4j + slf4j-api + 2.0.17 + + + ch.qos.logback + logback-classic + 1.5.18 + compile + + - final-project-module-3 - + quest_training + + maven-clean-plugin 3.4.0 - + maven-resources-plugin 3.3.1 @@ -44,6 +113,9 @@ maven-compiler-plugin 3.13.0 + + 23 + maven-surefire-plugin @@ -63,5 +135,14 @@ + + + org.apache.maven.plugins + maven-compiler-plugin + + 23 + + + diff --git a/src/main/java/quiz/command/Command.java b/src/main/java/quiz/command/Command.java new file mode 100644 index 0000000..02a8956 --- /dev/null +++ b/src/main/java/quiz/command/Command.java @@ -0,0 +1,5 @@ +package quiz.command; + +public interface Command { + void execute(); +} diff --git a/src/main/java/quiz/command/RedoCommand.java b/src/main/java/quiz/command/RedoCommand.java new file mode 100644 index 0000000..b751613 --- /dev/null +++ b/src/main/java/quiz/command/RedoCommand.java @@ -0,0 +1,25 @@ +package quiz.command; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quiz.history.QuestionsHistoryManager; + +public class RedoCommand implements Command { + private static final Logger logger = LoggerFactory.getLogger(RedoCommand.class); + + private final QuestionsHistoryManager memento; + + public RedoCommand(QuestionsHistoryManager memento) { + if (memento == null) { + logger.error("Memento cannot be null"); + throw new IllegalArgumentException("Memento cannot be null"); + } + + this.memento = memento; + } + + @Override + public void execute() { + memento.backup(); + } +} diff --git a/src/main/java/quiz/command/UndoCommand.java b/src/main/java/quiz/command/UndoCommand.java new file mode 100644 index 0000000..6ff8eae --- /dev/null +++ b/src/main/java/quiz/command/UndoCommand.java @@ -0,0 +1,25 @@ +package quiz.command; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quiz.history.QuestionsHistoryManager; + +public class UndoCommand implements Command { + private static final Logger logger = LoggerFactory.getLogger(UndoCommand.class); + + private final QuestionsHistoryManager memento; + + public UndoCommand(QuestionsHistoryManager memento) { + if (memento == null) { + logger.error("Memento cannot be null"); + throw new IllegalArgumentException("Memento cannot be null"); + } + + this.memento = memento; + } + + @Override + public void execute() { + memento.restore(); + } +} diff --git a/src/main/java/quiz/deserializer/Deserializer.java b/src/main/java/quiz/deserializer/Deserializer.java new file mode 100644 index 0000000..7da2297 --- /dev/null +++ b/src/main/java/quiz/deserializer/Deserializer.java @@ -0,0 +1,5 @@ +package quiz.deserializer; + +public interface Deserializer { + T deserializeFromFile(String filePath, Class clazz); +} diff --git a/src/main/java/quiz/deserializer/DeserializerBase.java b/src/main/java/quiz/deserializer/DeserializerBase.java new file mode 100644 index 0000000..4206bd7 --- /dev/null +++ b/src/main/java/quiz/deserializer/DeserializerBase.java @@ -0,0 +1,80 @@ +package quiz.deserializer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; + +public abstract class DeserializerBase implements Deserializer { + private static final Logger logger = LoggerFactory.getLogger(DeserializerBase.class); + + protected void validateFilePath(String filePath) { + if (filePath == null || filePath.isBlank()) { + logger.error("File path cannot be null or empty"); + throw new IllegalArgumentException("File path cannot be null or empty"); + } + } + + protected void validateClass(Class clazz) { + if (clazz == null) { + logger.error("Target class cannot be null"); + throw new IllegalArgumentException("Target class cannot be null"); + } + + if (clazz.isInterface()) { + throw new IllegalArgumentException("Target class cannot be an interface"); + } + + if (Modifier.isAbstract(clazz.getModifiers())) { + throw new IllegalArgumentException("Target class cannot be abstract"); + } + } + + protected InputStream loadResourceAsStream(String filePath) throws IOException { + InputStream fileStream = loadFromFileSystem(filePath); + if (fileStream != null) { + return fileStream; + } + + return loadFromClasspath(filePath); + } + + protected InputStream loadFromFileSystem(String filePath) throws IOException { + validateFilePath(filePath); + + Path path = Path.of(filePath).normalize(); + + if (!Files.exists(path)) { + logger.debug("File not found in filesystem: {}", path); + return null; + } + + if (!Files.isReadable(path)) { + logger.error("File exists but is not readable: {}", path); + throw new IOException("File exists but is not readable: " + path); + } + + logger.debug("Loading file from filesystem: {}", path); + return Files.newInputStream(path); + } + + protected InputStream loadFromClasspath(String resourcePath) { + validateFilePath(resourcePath); + + InputStream inputStream = Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream(resourcePath); + + if (inputStream == null) { + logger.error("Resource not found in classpath: {}", resourcePath); + throw new IllegalStateException("Resource not found in classpath: " + resourcePath); + } + + logger.debug("Loading resource from classpath: {}", resourcePath); + return inputStream; + } +} diff --git a/src/main/java/quiz/deserializer/DeserializerObjectFactory.java b/src/main/java/quiz/deserializer/DeserializerObjectFactory.java new file mode 100644 index 0000000..e04dc6b --- /dev/null +++ b/src/main/java/quiz/deserializer/DeserializerObjectFactory.java @@ -0,0 +1,34 @@ +package quiz.deserializer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quiz.deserializer.json.DeserializerJSONObjectMapper; + +public class DeserializerObjectFactory { + private static final Logger logger = LoggerFactory.getLogger(DeserializerObjectFactory.class); + + public T deserializerAndCreateObject(String filePath, Class clazz) { + Deserializer deserializer = getDeserializer(filePath); + return deserializer.deserializeFromFile(filePath, clazz); + } + + private Deserializer getDeserializer(String filePath) { + // Поки що тільки JSON + String extension = getFileExtension(filePath); + return switch (extension) { + case "json" -> new DeserializerJSONObjectMapper<>(); + default -> { + logger.error("Unsupported file format: {}", filePath); + throw new IllegalArgumentException("Only JSON format is supported"); + } + }; + } + + private String getFileExtension(String filePath) { + if (filePath == null || filePath.isBlank()) { + return ""; + } + int lastDotIndex = filePath.lastIndexOf('.'); + return (lastDotIndex > 0) ? filePath.substring(lastDotIndex + 1) : ""; + } +} diff --git a/src/main/java/quiz/deserializer/json/DeserializerJSONObjectMapper.java b/src/main/java/quiz/deserializer/json/DeserializerJSONObjectMapper.java new file mode 100644 index 0000000..63e36fd --- /dev/null +++ b/src/main/java/quiz/deserializer/json/DeserializerJSONObjectMapper.java @@ -0,0 +1,45 @@ +package quiz.deserializer.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quiz.deserializer.DeserializerBase; + +import java.io.IOException; +import java.io.InputStream; + +public class DeserializerJSONObjectMapper extends DeserializerBase { + private static final Logger logger = LoggerFactory.getLogger(DeserializerJSONObjectMapper.class); + + private final ObjectMapper mapper; + + public DeserializerJSONObjectMapper() { + this.mapper = new ObjectMapper(); + } + + DeserializerJSONObjectMapper(ObjectMapper mapper) { + this.mapper = mapper; + } + + @Override + public T deserializeFromFile(String filePath, Class clazz) { + logger.debug("Attempting to deserialize JSON file {} to {}", filePath, clazz.getName()); + + validateFilePath(filePath); + validateClass(clazz); + + try (InputStream inputStream = loadResourceAsStream(filePath)) + { + T object = mapper.readValue(inputStream, clazz); + if (object == null) { + logger.error("Deserialization failed: got null from file '{}'", filePath); + throw new IllegalStateException("Deserialization returned null from: " + filePath); + } + logger.debug("Successfully deserialized from JSON file '{}' into {}", filePath, object.getClass()); + return object; + } catch (IOException e) { + logger.error("Error reading JSON file: {}", filePath); + throw new IllegalStateException("Error reading JSON file: " + filePath, e); + } + } +} diff --git a/src/main/java/quiz/editor/Editor.java b/src/main/java/quiz/editor/Editor.java new file mode 100644 index 0000000..cc54e70 --- /dev/null +++ b/src/main/java/quiz/editor/Editor.java @@ -0,0 +1,65 @@ +package quiz.editor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quiz.command.Command; +import quiz.command.RedoCommand; +import quiz.command.UndoCommand; +import quiz.history.QuestionsHistoryManager; +import quiz.scenarios.Question; + +import java.util.List; +import java.util.Optional; + +public class Editor { + private static final Logger logger = LoggerFactory.getLogger(Editor.class); + + private final Command undo; + private final Command redo; + + private int currentIndex = -1; + + private final QuestionsHistoryManager memento; + + public Editor(QuestionsHistoryManager memento) { + if (memento == null) { + logger.error("Memento cannot be null"); + throw new IllegalArgumentException("Memento cannot be null"); + } + + this.memento = memento; + + this.undo = new UndoCommand(memento); + this.redo = new RedoCommand(memento); + } + + public Optional getNextObject() { + redo.execute(); + return getQuestion(++currentIndex); + } + + public Optional getPreviousObject() { + undo.execute(); + return getQuestion(--currentIndex); + } + + public Optional getCurrentObject() { + return getQuestion(currentIndex); + } + + public List getAllObjects() { + return memento.getAllQuestions(); + } + + private Optional getQuestion(int index) { + List questions = memento.getAllQuestions(); + if (index < 0 || index >= questions.size()) { + return Optional.empty(); + } + + Question question = questions.get(index); + return question == null ? + Optional.empty() : + Optional.of(question); + } +} diff --git a/src/main/java/quiz/history/History.java b/src/main/java/quiz/history/History.java new file mode 100644 index 0000000..81467ca --- /dev/null +++ b/src/main/java/quiz/history/History.java @@ -0,0 +1,33 @@ +package quiz.history; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Optional; + +public class History { + private static final Logger logger = LoggerFactory.getLogger(History.class); + + private final Deque mementos = new ArrayDeque<>(); + + public void save(Memento memento) { + if (memento == null) { + logger.error("Memento cannot be null"); + throw new IllegalArgumentException("Memento cannot be null"); + } + mementos.push(memento); + } + + public Optional restore() { + return mementos.isEmpty() ? + Optional.empty() : + Optional.of(mementos.pop()); + } + + public void clear() { + mementos.clear(); + } + +} diff --git a/src/main/java/quiz/history/Memento.java b/src/main/java/quiz/history/Memento.java new file mode 100644 index 0000000..596ee79 --- /dev/null +++ b/src/main/java/quiz/history/Memento.java @@ -0,0 +1,31 @@ +package quiz.history; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quiz.scenarios.Question; + +import java.util.ArrayList; +import java.util.List; + +public final class Memento { + private static final Logger logger = LoggerFactory.getLogger(Memento.class); + + private final List questionsClone; + + Memento(List questionsClone) { + if (questionsClone == null) { + logger.error("Questions cannot be null"); + throw new IllegalArgumentException("Questions cannot be null"); + } + + this.questionsClone = new ArrayList<>(); + + for (Question question : questionsClone) { + this.questionsClone.add(question.copy()); + } + } + + List getQuestions() { + return new ArrayList<>(questionsClone); + } +} diff --git a/src/main/java/quiz/history/QuestionsHistoryManager.java b/src/main/java/quiz/history/QuestionsHistoryManager.java new file mode 100644 index 0000000..b3955b7 --- /dev/null +++ b/src/main/java/quiz/history/QuestionsHistoryManager.java @@ -0,0 +1,51 @@ +package quiz.history; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quiz.scenarios.Question; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class QuestionsHistoryManager { + private static final Logger logger = LoggerFactory.getLogger(QuestionsHistoryManager.class); + + private List questions; + private final History history = new History(); + + public QuestionsHistoryManager() {} + + public QuestionsHistoryManager(List questions) { + replaceObjects(questions); + } + + public void replaceObjects(List questionsNew) { + if (questionsNew == null || questionsNew.isEmpty()) { + logger.error("List Questions cannot be null or empty"); + throw new IllegalArgumentException("List Questions cannot be null or empty"); + } + this.questions = new ArrayList<>(questionsNew); + } + + public List getAllQuestions() { + return List.copyOf(questions); + } + + public void backup() { + history.save(createMemento()); + } + + public void restore() { + Optional optional = history.restore(); + optional.ifPresent(this::restoreFromMemento); + } + + private Memento createMemento() { + return new Memento(questions); + } + + private void restoreFromMemento(Memento memento) { + this.questions = memento.getQuestions(); + } +} diff --git a/src/main/java/quiz/scenarios/Check.java b/src/main/java/quiz/scenarios/Check.java new file mode 100644 index 0000000..8747988 --- /dev/null +++ b/src/main/java/quiz/scenarios/Check.java @@ -0,0 +1,87 @@ +package quiz.scenarios; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.Optional; + +public class Check { + private static final Logger logger = LoggerFactory.getLogger(Check.class); + + private final String choice; + private final Boolean isValidChoice; + + private boolean isSelected = false; + + @JsonCreator + public Check( + @JsonProperty("choice") String choice, + @JsonProperty("isValidChoice") Boolean isValidChoice + ) { + if (choice == null || choice.isBlank()) { + logger.error("Choice text cannot be null or empty"); + throw new IllegalArgumentException("Choice text cannot be null or empty"); + } + + this.choice = choice; + this.isValidChoice = Optional.ofNullable(isValidChoice).orElse(false); + } + + private Check (Check origin) { + if (origin == null) { + logger.error("Check origin cannot be null"); + throw new IllegalArgumentException("Check origin cannot be null"); + } + + this.choice = origin.choice; + this.isValidChoice = origin.isValidChoice; + this.isSelected = origin.isSelected; + } + + public Check copy() { + return new Check(this); + } + + public String getChoice() { + return choice; + } + + @JsonProperty("isValidChoice") + public boolean isValidChoice() { + return isValidChoice; + } + + public boolean getSelected() { + return isSelected; + } + + public void setSelected(boolean selected) { + isSelected = selected; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Check check)) return false; + return Objects.equals(choice, check.choice) && + Objects.equals(isValidChoice, check.isValidChoice) && + Objects.equals(isSelected, check.isSelected); + } + + @Override + public int hashCode() { + return Objects.hash(choice, isValidChoice, isSelected); + } + + @Override + public String toString() { + return "Check{" + + "choice='" + choice + '\'' + + ", isValidChoice=" + isValidChoice + + ", isSelected=" + isSelected + + '}'; + } +} diff --git a/src/main/java/quiz/scenarios/Question.java b/src/main/java/quiz/scenarios/Question.java new file mode 100644 index 0000000..79179ea --- /dev/null +++ b/src/main/java/quiz/scenarios/Question.java @@ -0,0 +1,105 @@ +package quiz.scenarios; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +public class Question { + private static final Logger logger = LoggerFactory.getLogger(Question.class); + + private final String questionID; + private final String description; + + private final Set checks; + + @JsonCreator + public Question( + @JsonProperty("questionID") String questionID, + @JsonProperty("description") String description, + @JsonProperty("checks") Set checks + ) { + this.questionID = questionID; + this.description = description; + this.checks = validateAndInitializeCheckSet(checks); + } + + private Set validateAndInitializeCheckSet(Set checks) { + if (checks == null) { + logger.warn("Options set is null for step {}", questionID); + return Collections.emptySet(); + } + + checks.forEach((check) -> { + if (check == null) { + logger.error("Check value cannot be null"); + throw new IllegalArgumentException("Check value cannot be null"); + } + }); + + return checks; + } + + private Question(Question origin) { + if (origin == null) { + logger.error("Step origin cannot be null"); + throw new IllegalArgumentException("Step origin cannot be null"); + } + + this.questionID = origin.questionID; + this.description = origin.description; + + Set copySet = new HashSet<>(); + for (Check check : origin.checks) { + copySet.add(check.copy()); + } + this.checks = copySet; + } + + public Question copy() { + return new Question(this); + } + + public String getQuestionID() { + return questionID; + } + + public String getDescription() { + return description; + } + + public List getChecks() { + return new ArrayList<>(checks); + } + + public List getShuffleChecks() { + List list = getChecks(); + Collections.shuffle(list); + return list; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Question question)) return false; + return Objects.equals(questionID, question.questionID) && + Objects.equals(description, question.description) && + Objects.equals(checks, question.checks); + } + + @Override + public int hashCode() { + return Objects.hash(questionID, description, checks); + } + + @Override + public String toString() { + return "Question{" + + "questionID='" + questionID + '\'' + + ", description='" + description + '\'' + + ", checks=" + checks + + '}'; + } +} diff --git a/src/main/java/quiz/scenarios/QuestionsTemplate.java b/src/main/java/quiz/scenarios/QuestionsTemplate.java new file mode 100644 index 0000000..6952e42 --- /dev/null +++ b/src/main/java/quiz/scenarios/QuestionsTemplate.java @@ -0,0 +1,78 @@ +package quiz.scenarios; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +public class QuestionsTemplate { + private static final Logger logger = LoggerFactory.getLogger(QuestionsTemplate.class); + + protected Set questions; + + @JsonCreator + public QuestionsTemplate(@JsonProperty("questions") Set questions) { + this.questions = validateAndInitializeQuestionsMap(questions); + } + + private Set validateAndInitializeQuestionsMap(Set questions) { + if (questions == null) { + logger.warn("Questions set is null. Return Questions set empty"); + return Collections.emptySet(); + } + + questions.forEach((question) -> { + if (question == null) { + logger.error("Question value cannot be null"); + throw new IllegalArgumentException("Question value cannot be null"); + } + }); + + return questions; + } + + private QuestionsTemplate(QuestionsTemplate origin) { + Set copySet = new HashSet<>(); + for (Question question : origin.questions) { + copySet.add(question.copy()); + } + this.questions = copySet; + } + + public QuestionsTemplate copy() { + return new QuestionsTemplate(this); + } + + public int size() { + return questions.size(); + } + + public List getQuestions() { + return new ArrayList<>(questions); + } + + public List getShuffleQuestions() { + List list = getQuestions(); + Collections.shuffle(list); + return list; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof QuestionsTemplate that)) return false; + return Objects.equals(questions, that.questions); + } + + public int hashCode() { + return Objects.hashCode(questions); + } + + @Override + public String toString() { + return "QuestionsTemplate{" + + "questions=" + questions + + '}'; + } +} diff --git a/src/main/java/quiz/servlet/flags/LogicServlet.java b/src/main/java/quiz/servlet/flags/LogicServlet.java new file mode 100644 index 0000000..f3e5e66 --- /dev/null +++ b/src/main/java/quiz/servlet/flags/LogicServlet.java @@ -0,0 +1,123 @@ +package quiz.servlet.flags; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quiz.editor.Editor; +import quiz.scenarios.Check; +import quiz.scenarios.Question; + +import java.io.IOException; +import java.util.Optional; + +@WebServlet(name = "LogicServlet", value = "/logic") +public class LogicServlet extends HttpServlet { + private static final Logger logger = LoggerFactory.getLogger(LogicServlet.class); + + private static final String BUTTON_BACK = "back"; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + HttpSession session = req.getSession(false); + + try { + if (isSessionInvalid(session, resp, req)) { + return; + } + + getAttributeFromSession(session, "question", Question.class) + .ifPresent(question -> applyUserChoice(question, req)); + + Editor editor = getAttributeFromSession(session, "editor", Editor.class) + .orElseThrow(() -> new IllegalStateException("Editor is missing in session")); + + int questionIndex = getAttributeFromSession(session, "questionIndex", Integer.class) + .orElseThrow(() -> new IllegalStateException("QuestionIndex is missing in session")); + + String action = req.getParameter("action"); + Optional nextQuestion = BUTTON_BACK.equals(action) + ? editor.getPreviousObject() + : editor.getNextObject(); + + if (nextQuestion.isEmpty()) { + logger.info("No more questions, redirect to result page"); + session.setAttribute("questions", editor.getAllObjects()); + resp.sendRedirect(req.getContextPath() + "/result.jsp"); + return; + } + + int updatedIndex = BUTTON_BACK.equals(action) ? questionIndex - 1 : questionIndex + 1; + + updateSession(session, editor, nextQuestion.get(), updatedIndex); + + resp.sendRedirect(req.getContextPath() + "/logic"); + } catch (IOException | IllegalStateException e) { + logger.error("Error processing request: ", e); + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException { + HttpSession session = req.getSession(false); + try { + if (isSessionInvalid(session, resp, req)) { + return; + } + + getAttributeFromSession(session, "question", Question.class) + .orElseThrow(() -> new IllegalStateException("Current question is missing")); + + req.getRequestDispatcher("/logic.jsp").forward(req, resp); + } catch (IOException | IllegalStateException e) { + logger.error("Error processing request: ", e); + } + } + + private boolean isSessionInvalid(HttpSession session, HttpServletResponse resp, HttpServletRequest req) throws IOException { + if (session == null) { + logger.warn("HttpSession does not exist"); + resp.sendRedirect(req.getContextPath() + "/restart"); + return true; + } + return false; + } + + private void applyUserChoice(Question question, HttpServletRequest req) { + String selectedChoice = req.getParameter("user-choice"); + if (selectedChoice != null) { + logger.debug("String selectedChoice not null"); + for (Check check : question.getChecks()) { + logger.debug("{} set selected {}", check.getChoice(), selectedChoice.equals(check.getChoice())); + check.setSelected(selectedChoice.equals(check.getChoice())); + } + } + } + + private Optional getAttributeFromSession(HttpSession session, String attributeName, Class expectedType) { + Object attribute = session.getAttribute(attributeName); + if (attribute == null) { + logger.warn("Session attribute '{}' is null", attributeName); + return Optional.empty(); + } + + if (!expectedType.isInstance(attribute)) { + logger.error("Attribute '{}' is not a {} (actual type: {})", + attributeName, expectedType.getSimpleName(), attribute.getClass().getName()); + throw new IllegalStateException("Attribute '" + attributeName + "' is not a " + + expectedType.getSimpleName() + " (actual type: " + attribute.getClass().getName() + ")"); + } + + return Optional.of(expectedType.cast(attribute)); + } + + private void updateSession(HttpSession session, Editor editor, Question question, int questionIndex) { + session.setAttribute("editor", editor); + session.setAttribute("question", question); + session.setAttribute("questionIndex", questionIndex); + } +} \ No newline at end of file diff --git a/src/main/java/quiz/servlet/flags/RestartServlet.java b/src/main/java/quiz/servlet/flags/RestartServlet.java new file mode 100644 index 0000000..b5d457b --- /dev/null +++ b/src/main/java/quiz/servlet/flags/RestartServlet.java @@ -0,0 +1,17 @@ +package quiz.servlet.flags; + +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "RestartServlet", value = "/restart") +public class RestartServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + req.getSession(false).invalidate(); + resp.sendRedirect(req.getContextPath() + "/start"); + } +} diff --git a/src/main/java/quiz/servlet/flags/ResultServlet.java b/src/main/java/quiz/servlet/flags/ResultServlet.java new file mode 100644 index 0000000..fc2c8d5 --- /dev/null +++ b/src/main/java/quiz/servlet/flags/ResultServlet.java @@ -0,0 +1,17 @@ +package quiz.servlet.flags; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "ResultServlet", value = "/result") +public class ResultServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + req.getRequestDispatcher("/result.jsp").forward(req, resp); + } +} diff --git a/src/main/java/quiz/servlet/flags/StartServlet.java b/src/main/java/quiz/servlet/flags/StartServlet.java new file mode 100644 index 0000000..fe08bf1 --- /dev/null +++ b/src/main/java/quiz/servlet/flags/StartServlet.java @@ -0,0 +1,62 @@ +package quiz.servlet.flags; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quiz.deserializer.DeserializerObjectFactory; +import quiz.editor.Editor; +import quiz.history.QuestionsHistoryManager; +import quiz.scenarios.Question; +import quiz.scenarios.QuestionsTemplate; + +import java.io.IOException; +import java.util.List; + +@WebServlet(name = "StartServlet", value = "/start") +public class StartServlet extends HttpServlet { + private static final Logger logger = LoggerFactory.getLogger(StartServlet.class); + + private static final String FLAGS_SCENARIO_PATH = "scenarios/flags.json"; + + private static final int FIRST_QUESTION_INDEX = 0; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException { + logger.debug("Initializing StartServlet.doGet()"); + HttpSession session = createOrGetSession(req); + + try { + initializeQuizSession(session); + req.getRequestDispatcher("/start.jsp").forward(req, resp); + } catch (IOException | IllegalStateException e) { + logger.error("Initialization failed: {}", e.getMessage()); + session.invalidate(); + } + } + + private HttpSession createOrGetSession(HttpServletRequest req) { + HttpSession session = req.getSession(true); + logger.debug("Session created/retrieved: {}", session.getId()); + return session; + } + + private void initializeQuizSession(HttpSession session) { + DeserializerObjectFactory deserializer = new DeserializerObjectFactory(); + QuestionsTemplate questionsTemplate = deserializer.deserializerAndCreateObject(FLAGS_SCENARIO_PATH, QuestionsTemplate.class); + + List questions = questionsTemplate.getShuffleQuestions(); + + QuestionsHistoryManager memento = new QuestionsHistoryManager(questions); + Editor editor = new Editor(memento); + + session.setAttribute("editor", editor); + session.setAttribute("totalQuestionsCount", questionsTemplate.size()); + session.setAttribute("questionIndex", FIRST_QUESTION_INDEX); + logger.debug("Session initialized with {} Questions", questionsTemplate.size()); + } +} diff --git a/src/main/resources/scenarios/flags.json b/src/main/resources/scenarios/flags.json new file mode 100644 index 0000000..d0aeca7 --- /dev/null +++ b/src/main/resources/scenarios/flags.json @@ -0,0 +1,104 @@ +{ + "questions": [ + { + "questionID": "Україна", + "description": "ukraine_flag.webp", + "checks": [ + { "choice": "Польща", "isValidChoice": false }, + { "choice": "Україна", "isValidChoice": true }, + { "choice": "Швеція", "isValidChoice": false }, + { "choice": "Фінляндія", "isValidChoice": false } + ] + }, + { + "questionID": "США", + "description": "usa_flag.webp", + "checks": [ + { "choice": "Велика Британія", "isValidChoice": false }, + { "choice": "Австралія", "isValidChoice": false }, + { "choice": "США", "isValidChoice": true }, + { "choice": "Канада", "isValidChoice": false } + ] + }, + { + "questionID": "Японія", + "description": "japan_flag.webp", + "checks": [ + { "choice": "Китай", "isValidChoice": false }, + { "choice": "Південна Корея", "isValidChoice": false }, + { "choice": "Вʼєтнам", "isValidChoice": false }, + { "choice": "Японія", "isValidChoice": true } + ] + }, + { + "questionID": "Канада", + "description": "canada_flag.webp", + "checks": [ + { "choice": "Швейцарія", "isValidChoice": false }, + { "choice": "Канада", "isValidChoice": true }, + { "choice": "Норвегія", "isValidChoice": false }, + { "choice": "Данія", "isValidChoice": false } + ] + }, + { + "questionID": "Франція", + "description": "france_flag.webp", + "checks": [ + { "choice": "Бельгія", "isValidChoice": false }, + { "choice": "Франція", "isValidChoice": true }, + { "choice": "Нідерланди", "isValidChoice": false }, + { "choice": "Люксембург", "isValidChoice": false } + ] + }, + { + "questionID": "Велика Британія", + "description": "united_kingdom_flag.webp", + "checks": [ + { "choice": "Ірландія", "isValidChoice": false }, + { "choice": "Шотландія", "isValidChoice": false }, + { "choice": "Велика Британія", "isValidChoice": true }, + { "choice": "Уельс", "isValidChoice": false } + ] + }, + { + "questionID": "Австралія", + "description": "australia_flag.webp", + "checks": [ + { "choice": "Фіджі", "isValidChoice": false }, + { "choice": "Австралія", "isValidChoice": true }, + { "choice": "Нова Зеландія", "isValidChoice": false }, + { "choice": "Самоа", "isValidChoice": false } + ] + }, + { + "questionID": "Бразилія", + "description": "brazil_flag.webp", + "checks": [ + { "choice": "Перу", "isValidChoice": false }, + { "choice": "Бразилія", "isValidChoice": true }, + { "choice": "Аргентина", "isValidChoice": false }, + { "choice": "Чилі", "isValidChoice": false } + ] + }, + { + "questionID": "ПАР", + "description": "south_africa_flag.webp", + "checks": [ + { "choice": "Кенія", "isValidChoice": false }, + { "choice": "Нігерія", "isValidChoice": false }, + { "choice": "ПАР", "isValidChoice": true }, + { "choice": "Єгипет", "isValidChoice": false } + ] + }, + { + "questionID": "Індія", + "description": "india_flag.webp", + "checks": [ + { "choice": "Бангладеш", "isValidChoice": false }, + { "choice": "Індія", "isValidChoice": true }, + { "choice": "Пакистан", "isValidChoice": false }, + { "choice": "Шрі-Ланка", "isValidChoice": false } + ] + } + ] +} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index b9c921f..daf1c08 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -7,4 +7,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"> Archetype Created Web Application + + + start + diff --git a/src/main/webapp/images/australia_flag.webp b/src/main/webapp/images/australia_flag.webp new file mode 100644 index 0000000..47d113b Binary files /dev/null and b/src/main/webapp/images/australia_flag.webp differ diff --git a/src/main/webapp/images/brazil_flag.webp b/src/main/webapp/images/brazil_flag.webp new file mode 100644 index 0000000..bf5b0f3 Binary files /dev/null and b/src/main/webapp/images/brazil_flag.webp differ diff --git a/src/main/webapp/images/canada_flag.webp b/src/main/webapp/images/canada_flag.webp new file mode 100644 index 0000000..3c1796a Binary files /dev/null and b/src/main/webapp/images/canada_flag.webp differ diff --git a/src/main/webapp/images/france_flag.webp b/src/main/webapp/images/france_flag.webp new file mode 100644 index 0000000..a1ab4fa Binary files /dev/null and b/src/main/webapp/images/france_flag.webp differ diff --git a/src/main/webapp/images/india_flag.webp b/src/main/webapp/images/india_flag.webp new file mode 100644 index 0000000..1d64912 Binary files /dev/null and b/src/main/webapp/images/india_flag.webp differ diff --git a/src/main/webapp/images/japan_flag.webp b/src/main/webapp/images/japan_flag.webp new file mode 100644 index 0000000..aa696ff Binary files /dev/null and b/src/main/webapp/images/japan_flag.webp differ diff --git a/src/main/webapp/images/south_africa_flag.webp b/src/main/webapp/images/south_africa_flag.webp new file mode 100644 index 0000000..5bfb9f5 Binary files /dev/null and b/src/main/webapp/images/south_africa_flag.webp differ diff --git a/src/main/webapp/images/ukraine_flag.webp b/src/main/webapp/images/ukraine_flag.webp new file mode 100644 index 0000000..0c62225 Binary files /dev/null and b/src/main/webapp/images/ukraine_flag.webp differ diff --git a/src/main/webapp/images/united_kingdom_flag.webp b/src/main/webapp/images/united_kingdom_flag.webp new file mode 100644 index 0000000..2a23bdd Binary files /dev/null and b/src/main/webapp/images/united_kingdom_flag.webp differ diff --git a/src/main/webapp/images/usa_flag.webp b/src/main/webapp/images/usa_flag.webp new file mode 100644 index 0000000..a131ce1 Binary files /dev/null and b/src/main/webapp/images/usa_flag.webp differ diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp deleted file mode 100644 index ee89d62..0000000 --- a/src/main/webapp/index.jsp +++ /dev/null @@ -1,5 +0,0 @@ - - -

<%= "Hello World!" %>

- - diff --git a/src/main/webapp/logic.jsp b/src/main/webapp/logic.jsp new file mode 100644 index 0000000..8ac6275 --- /dev/null +++ b/src/main/webapp/logic.jsp @@ -0,0 +1,88 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + + + + + + + Впізнай країну за прапором + + + + + + + + + + +
+
+

Впізнай країну за прапором

+ +

Питання: ${questionIndex} / ${totalQuestionsCount}

+
+
+ + + +
+ + + + Flag of ${question.description} + +
+ +
+ +
+
+
+
+
+ + + + + + +
+
+
+ +
+ +

Немає даних для відображення

+
+
+
+ + \ No newline at end of file diff --git a/src/main/webapp/result.jsp b/src/main/webapp/result.jsp new file mode 100644 index 0000000..22a7e6e --- /dev/null +++ b/src/main/webapp/result.jsp @@ -0,0 +1,87 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + + + + + + + Впізнай країну за прапором + + + + + + + +
+

Результат

+ + + + + + + Flag of ${question.description} + +
+ + + + + + + + + + + + + + + + +
+
+ ${fn:escapeXml(check.choice)} +
+ + Ваш вибір + + + + Правильний + + + + Неправильний + +
+
+
+
+
+
+
+
+
+ +

Немає даних для відображення

+
+
+
+
+ +
+
+
+ + \ No newline at end of file diff --git a/src/main/webapp/start.jsp b/src/main/webapp/start.jsp new file mode 100644 index 0000000..112beee --- /dev/null +++ b/src/main/webapp/start.jsp @@ -0,0 +1,29 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + + + Впізнай країну за прапором + + + + + +
+

Впізнай країну за прапором

+
+

Перевір свої знання географії!

+

Цей тест покаже, наскільки добре ти розпізнаєш прапори країн світу. + У кожному запитанні буде зображення прапора — обери правильну назву країни з кількох варіантів.

+

Результати з’являться наприкінці тесту.

+
+
+<%-- Який метод викликати під час переходу на інший сервлет get чи post--%> +
+ +
+
+
+ + diff --git a/src/main/webapp/static/styles_logic.css b/src/main/webapp/static/styles_logic.css new file mode 100644 index 0000000..d3cd550 --- /dev/null +++ b/src/main/webapp/static/styles_logic.css @@ -0,0 +1,33 @@ +body, html { + height: 100%; + margin: 0; +} +.full-screen-color { + min-height: 100vh; + background-color: #81674d; + color: white; + display: flex; + flex-direction: column; + justify-content: center; + padding: 2rem; + font-size: 1.25rem; +} +img.object-fit-fill.border.rounded { + object-fit: cover; + border: 2px solid #dee2e6; + border-radius: 12px; + width: auto; + max-width: 300px; + height: auto; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} +h1 { + font-size: 2.5rem; +} +.form-check-label { + font-size: 1.2rem; +} +.btn { + font-size: 1.2rem; + padding: 0.6rem 2rem; +} \ No newline at end of file diff --git a/src/main/webapp/static/styles_result.css b/src/main/webapp/static/styles_result.css new file mode 100644 index 0000000..d2527ff --- /dev/null +++ b/src/main/webapp/static/styles_result.css @@ -0,0 +1,23 @@ +.full-screen-color { + min-height: 100vh; + background-color: #81674d; + color: white; + display: flex; + flex-direction: column; + justify-content: center; + padding: 2rem; + font-size: 1.25rem; +} +img.object-fit-fill.border.rounded { + object-fit: cover; + border: 2px solid #dee2e6; + border-radius: 12px; + width: auto; + max-width: 300px; + height: auto; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} +.btn { + font-size: 1.2rem; + padding: 0.6rem 2rem; +} \ No newline at end of file diff --git a/src/main/webapp/static/styles_start.css b/src/main/webapp/static/styles_start.css new file mode 100644 index 0000000..69a357c --- /dev/null +++ b/src/main/webapp/static/styles_start.css @@ -0,0 +1,24 @@ +body, html { + height: 100%; + margin: 0; +} +.full-screen-color { + min-height: 100vh; + background-color: #81674d; + color: white; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + padding: 2rem; +} +h1 { + font-size: 3rem; +} +p { + font-size: 1.5rem; +} +.btn { + font-size: 1.2rem; + padding: 0.6rem 2rem; +} \ No newline at end of file diff --git a/src/test/java/quiz/command/RedoCommandTest.java b/src/test/java/quiz/command/RedoCommandTest.java new file mode 100644 index 0000000..0abd95a --- /dev/null +++ b/src/test/java/quiz/command/RedoCommandTest.java @@ -0,0 +1,40 @@ +package quiz.command; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import quiz.history.QuestionsHistoryManager; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class RedoCommandTest { + + private QuestionsHistoryManager mockHistoryManager; + private RedoCommand redoCommand; + + @BeforeEach + void setUp() { + mockHistoryManager = mock(QuestionsHistoryManager.class); + redoCommand = new RedoCommand(mockHistoryManager); + } + + @Test + void testConstructor_shouldInitializeWithValidManager() { + assertNotNull(redoCommand, "RedoCommand should be properly initialized"); + } + + @Test + void testConstructor_withNullManager_shouldThrowException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new RedoCommand(null); + }); + assertEquals("Memento cannot be null", exception.getMessage(), "Should throw exception for null memento"); + } + + @Test + void testExecute_shouldCallRestore() { + redoCommand.execute(); + + verify(mockHistoryManager, times(1)).backup(); + } +} diff --git a/src/test/java/quiz/command/UndoCommandTest.java b/src/test/java/quiz/command/UndoCommandTest.java new file mode 100644 index 0000000..72422df --- /dev/null +++ b/src/test/java/quiz/command/UndoCommandTest.java @@ -0,0 +1,40 @@ +package quiz.command; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import quiz.history.QuestionsHistoryManager; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class UndoCommandTest { + + private QuestionsHistoryManager mockHistoryManager; + private UndoCommand undoCommand; + + @BeforeEach + void setUp() { + mockHistoryManager = mock(QuestionsHistoryManager.class); + undoCommand = new UndoCommand(mockHistoryManager); + } + + @Test + void testConstructor_shouldInitializeWithValidManager() { + assertNotNull(undoCommand, "UndoCommand should be properly initialized"); + } + + @Test + void testConstructor_withNullManager_shouldThrowException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new UndoCommand(null); + }); + assertEquals("Memento cannot be null", exception.getMessage(), "Should throw exception for null memento"); + } + + @Test + void testExecute_shouldCallBackup() { + undoCommand.execute(); + + verify(mockHistoryManager, times(1)).restore(); + } +} diff --git a/src/test/java/quiz/deserializer/DeserializerBaseTest.java b/src/test/java/quiz/deserializer/DeserializerBaseTest.java new file mode 100644 index 0000000..f06e8ef --- /dev/null +++ b/src/test/java/quiz/deserializer/DeserializerBaseTest.java @@ -0,0 +1,155 @@ +package quiz.deserializer; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class DeserializerBaseTest { + + private DeserializerBase createStubDeserializer(T returnValue) { + return new DeserializerBase<>() { + @Override + public T deserializeFromFile(String filePath, Class clazz) { + return returnValue; + } + }; + } + + @Test + void givenNullFilePath_whenValidate_thenThrowException() { + DeserializerBase deserializer = createStubDeserializer(null); + + assertThrows(IllegalArgumentException.class, () -> deserializer.validateFilePath(null)); + assertDoesNotThrow(() -> deserializer.validateFilePath("valid/path.json")); + } + + @Test + void givenEmptyOrBlankFilePath_whenValidate_thenThrowException() { + DeserializerBase deserializer = createStubDeserializer(null); + + assertThrows(IllegalArgumentException.class, () -> deserializer.validateFilePath("")); + assertThrows(IllegalArgumentException.class, () -> deserializer.validateFilePath(" ")); + assertDoesNotThrow(() -> deserializer.validateFilePath("valid/path.json")); + } + + @Test + void givenBlankFilePath_whenValidate_thenThrowException() { + DeserializerBase deserializer = createStubDeserializer(null); + + assertThrows(IllegalArgumentException.class, () -> deserializer.validateFilePath(" ")); + assertThrows(IllegalArgumentException.class, () -> deserializer.validateFilePath("")); + assertThrows(IllegalArgumentException.class, () -> deserializer.validateFilePath(" \t ")); + } + + @Test + void givenNullClass_whenValidate_thenThrowException() { + DeserializerBase deserializer = createStubDeserializer(null); + + assertThrows(IllegalArgumentException.class, () -> deserializer.validateClass(null)); + assertDoesNotThrow(() -> deserializer.validateClass(String.class)); + } + + @Test + void givenInterfaceClass_whenValidate_thenThrowException() { + interface TestInterface {} + + DeserializerBase deserializer = createStubDeserializer(null); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> deserializer.validateClass(TestInterface.class)); + assertEquals("Target class cannot be an interface", exception.getMessage()); + } + + @Test + void givenAbstractClass_whenValidate_thenThrowException() { + abstract class TestAbstract {} + + DeserializerBase deserializer = createStubDeserializer(null); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> deserializer.validateClass(TestAbstract.class)); + assertEquals("Target class cannot be abstract", exception.getMessage()); + } + + @Test + void givenValidResource_whenLoadFromClasspath_thenReturnInputStream() { + // Arrange + ClassLoader mockClassLoader = mock(ClassLoader.class); + InputStream mockInputStream = mock(InputStream.class); + + try (MockedStatic threadMock = mockStatic(Thread.class)) { + threadMock.when(Thread::currentThread).thenReturn(Thread.currentThread()); + when(Thread.currentThread().getContextClassLoader()).thenReturn(mockClassLoader); + when(mockClassLoader.getResourceAsStream("valid/resource.txt")).thenReturn(mockInputStream); + + DeserializerBase deserializer = createStubDeserializer(null); + + // Act + InputStream result = deserializer.loadFromClasspath("valid/resource.txt"); + + // Assert + assertNotNull(result); + assertSame(mockInputStream, result); + verify(mockClassLoader).getResourceAsStream("valid/resource.txt"); + } + } + + @Test + void givenInvalidResource_whenLoadFromClasspath_thenThrowException() { + try (MockedStatic threadMock = mockStatic(Thread.class)) { + Thread mockThread = mock(Thread.class); + ClassLoader mockClassLoader = mock(ClassLoader.class); + + threadMock.when(Thread::currentThread).thenReturn(mockThread); + when(mockThread.getContextClassLoader()).thenReturn(mockClassLoader); + when(mockClassLoader.getResourceAsStream("invalid/resource.txt")).thenReturn(null); + + DeserializerBase deserializer = createStubDeserializer(null); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> deserializer.loadFromClasspath("invalid/resource.txt")); + + assertEquals("Resource not found in classpath: invalid/resource.txt", exception.getMessage()); + verify(mockClassLoader).getResourceAsStream("invalid/resource.txt"); + } + } + + @Test + void givenValidResource_whenLoadFromFileSystem_thenReturnInputStream() { + try (MockedStatic filesMock = mockStatic(Files.class); + MockedStatic pathMock = mockStatic(Path.class)) { + + // Arrange + Path mockPath = mock(Path.class); + Path normalizedPath = mock(Path.class); + InputStream mockInputStream = mock(InputStream.class); + + pathMock.when(() -> Path.of("valid/resource.txt")).thenReturn(mockPath); + when(mockPath.normalize()).thenReturn(normalizedPath); + + filesMock.when(() -> Files.exists(normalizedPath)).thenReturn(true); + filesMock.when(() -> Files.isReadable(normalizedPath)).thenReturn(true); + filesMock.when(() -> Files.newInputStream(normalizedPath)).thenReturn(mockInputStream); + + DeserializerBase deserializer = createStubDeserializer(null); + + assertDoesNotThrow(() -> { + try (InputStream result = deserializer.loadFromFileSystem("valid/resource.txt")) { + assertNotNull(result); + assertSame(mockInputStream, result); + } + }); + + // Verify + filesMock.verify(() -> Files.exists(normalizedPath)); + filesMock.verify(() -> Files.isReadable(normalizedPath)); + filesMock.verify(() -> Files.newInputStream(normalizedPath)); + } + } +} diff --git a/src/test/java/quiz/deserializer/DeserializerObjectFactoryTest.java b/src/test/java/quiz/deserializer/DeserializerObjectFactoryTest.java new file mode 100644 index 0000000..6e60c12 --- /dev/null +++ b/src/test/java/quiz/deserializer/DeserializerObjectFactoryTest.java @@ -0,0 +1,67 @@ +package quiz.deserializer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class DeserializerObjectFactoryTest { + + private DeserializerObjectFactory factory; + + @BeforeEach + void setUp() { + factory = new DeserializerObjectFactory(); + } + + @ParameterizedTest + @NullAndEmptySource + void shouldThrowWhenFilePathIsNullOrEmpty(String filePath) { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> factory.deserializerAndCreateObject(filePath, Object.class) + ); + assertEquals("File path cannot be null or blank.", exception.getMessage()); + } + + @ParameterizedTest + @NullSource + void shouldThrowWhenClassIsNull(Class clazz) { + String filePath = "test.json"; + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> factory.deserializerAndCreateObject(filePath, clazz) + ); + assertEquals("Target class cannot be null", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = { + "test.xml", + "test.doc", + "test.txt", + "test.yaml", + "test.yml", + "test.csv", + "test.ini", + "test.exe", + "test.bin", + "test", + "test.", + "test.json.backup", + "testjson", + "test.jsonn", + "TEST.JSON", + "тест.json" + }) + void shouldThrowWhenFilePathHasUnsupportedExtension(String filePath) { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> factory.deserializerAndCreateObject(filePath, Object.class) + ); + assertEquals("Only JSON format is supported.", exception.getMessage()); + } +} diff --git a/src/test/java/quiz/deserializer/json/DeserializerJSONObjectMapperTest.java b/src/test/java/quiz/deserializer/json/DeserializerJSONObjectMapperTest.java new file mode 100644 index 0000000..1bf62d1 --- /dev/null +++ b/src/test/java/quiz/deserializer/json/DeserializerJSONObjectMapperTest.java @@ -0,0 +1,82 @@ +package quiz.deserializer.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class DeserializerJSONObjectMapperTest { + + @Test + void testDeserializeFromFile_withMockedMapperAndRealInputStream() throws IOException { + String json = "{\"name\":\"Charlie\",\"age\":40}"; + + try (InputStream realStream = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8))) { + ObjectMapper mockMapper = mock(ObjectMapper.class); + Person expected = new Person("Charlie", 40); + + when(mockMapper.readValue(any(InputStream.class), eq(Person.class))).thenReturn(expected); + + DeserializerJSONObjectMapper deserializer = new DeserializerJSONObjectMapper<>(mockMapper) { + @Override + protected InputStream loadResourceAsStream(String path) { + return realStream; + } + }; + + Person actual = deserializer.deserializeFromFile("dummy.json", Person.class); + + assertEquals(expected, actual); + verify(mockMapper).readValue(any(InputStream.class), eq(Person.class)); + } + } + + private static class Person { + private String name; + private int age; + + public Person() {} + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Person person)) return false; + return age == person.age && Objects.equals(name, person.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } + } +} \ No newline at end of file diff --git a/src/test/java/quiz/editor/EditorTest.java b/src/test/java/quiz/editor/EditorTest.java new file mode 100644 index 0000000..478dd45 --- /dev/null +++ b/src/test/java/quiz/editor/EditorTest.java @@ -0,0 +1,100 @@ +package quiz.editor; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import quiz.history.QuestionsHistoryManager; +import quiz.scenarios.Question; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class EditorTest { + + private QuestionsHistoryManager mockHistoryManager; + private Editor editor; + + @BeforeEach + void setUp() { + mockHistoryManager = mock(QuestionsHistoryManager.class); + editor = new Editor(mockHistoryManager); + } + + @Test + void testConstructor_shouldInitializeWithValidHistoryManager() { + assertNotNull(editor, "Editor should be properly initialized"); + } + + @Test + void testConstructor_withNullHistoryManager_shouldThrowException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new Editor(null); + }); + assertEquals("Memento cannot be null", exception.getMessage(), "Should throw exception for null memento"); + } + + @Test + void testGetNextObject_shouldReturnNextQuestion() { + Question mockQuestion = mock(Question.class); + when(mockHistoryManager.getAllQuestions()).thenReturn(List.of(mockQuestion)); + + Optional result = editor.getNextObject(); + + assertTrue(result.isPresent(), "Next question should be present"); + assertEquals(mockQuestion, result.get(), "Returned question should match the mock"); + } + + @Test + void testGetNextObject_shouldReturnEmptyIfOutOfBounds() { + when(mockHistoryManager.getAllQuestions()).thenReturn(List.of()); + + Optional result = editor.getNextObject(); + + assertFalse(result.isPresent(), "Next question should be empty if out of bounds"); + } + + @Test + void testGetPreviousObject_shouldReturnPreviousQuestion() { + Question mockQuestion = mock(Question.class); + when(mockHistoryManager.getAllQuestions()).thenReturn(List.of(mockQuestion)); + + Optional result = editor.getPreviousObject(); + + assertTrue(result.isPresent(), "Previous question should be present"); + assertEquals(mockQuestion, result.get(), "Returned question should match the mock"); + } + + @Test + void testGetPreviousObject_shouldReturnEmptyIfOutOfBounds() { + when(mockHistoryManager.getAllQuestions()).thenReturn(List.of()); + + Optional result = editor.getPreviousObject(); + + assertFalse(result.isPresent(), "Previous question should be empty if out of bounds"); + } + + @Test + void testGetCurrentObject_shouldReturnCurrentQuestion() { + Question mockQuestion = mock(Question.class); + when(mockHistoryManager.getAllQuestions()).thenReturn(List.of(mockQuestion)); + + Optional result = editor.getCurrentObject(); + + assertTrue(result.isPresent(), "Current question should be present"); + assertEquals(mockQuestion, result.get(), "Returned question should match the mock"); + } + + @Test + void testGetAllObjects_shouldReturnAllQuestions() { + Question mockQuestion = mock(Question.class); + when(mockHistoryManager.getAllQuestions()).thenReturn(List.of(mockQuestion)); + + List result = editor.getAllObjects(); + + assertNotNull(result, "List of questions should not be null"); + assertEquals(1, result.size(), "The list should contain one question"); + assertEquals(mockQuestion, result.get(0), "The question in the list should match the mock"); + } +} diff --git a/src/test/java/quiz/history/HistoryTest.java b/src/test/java/quiz/history/HistoryTest.java new file mode 100644 index 0000000..2101bf9 --- /dev/null +++ b/src/test/java/quiz/history/HistoryTest.java @@ -0,0 +1,66 @@ +package quiz.history; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class HistoryTest { + + private History history; + private Memento mockMemento; + + @BeforeEach + void setUp() { + history = new History(); + mockMemento = mock(Memento.class); + } + + @Test + void save_shouldStoreMemento() { + history.save(mockMemento); + Optional restored = history.restore(); + + assertTrue(restored.isPresent(), "Memento should be present after save"); + assertEquals(mockMemento, restored.get(), "Restored memento should match the one saved"); + } + + @Test + void save_shouldThrowExceptionWhenNull() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + history.save(null); + }); + + assertEquals("Memento cannot be null", exception.getMessage()); + } + + @Test + void restore_shouldReturnEmptyOptionalWhenNoMemento() { + Optional restored = history.restore(); + assertTrue(restored.isEmpty(), "Should return empty if no memento is saved"); + } + + @Test + void restore_shouldReturnInLIFOOrder() { + Memento m1 = mock(Memento.class); + Memento m2 = mock(Memento.class); + + history.save(m1); + history.save(m2); + + assertEquals(m2, history.restore().orElse(null), "Should return last pushed memento first"); + assertEquals(m1, history.restore().orElse(null), "Should return first pushed memento last"); + assertTrue(history.restore().isEmpty(), "Should be empty after all mementos restored"); + } + + @Test + void clear_shouldRemoveAllMementos() { + history.save(mockMemento); + history.clear(); + + assertTrue(history.restore().isEmpty(), "All mementos should be cleared"); + } +} diff --git a/src/test/java/quiz/history/MementoTest.java b/src/test/java/quiz/history/MementoTest.java new file mode 100644 index 0000000..723fbed --- /dev/null +++ b/src/test/java/quiz/history/MementoTest.java @@ -0,0 +1,65 @@ +package quiz.history; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import quiz.scenarios.Question; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class MementoTest { + + private Question mockQuestion1; + private Question mockQuestion2; + + private List originalList; + + @BeforeEach + void setUp() { + mockQuestion1 = mock(Question.class); + mockQuestion2 = mock(Question.class); + + when(mockQuestion1.copy()).thenReturn(mockQuestion1); + when(mockQuestion2.copy()).thenReturn(mockQuestion2); + + originalList = new ArrayList<>(); + originalList.add(mockQuestion1); + originalList.add(mockQuestion2); + } + + @Test + void constructor_shouldCopyQuestions() { + Memento memento = new Memento(originalList); + + List questions = memento.getQuestions(); + + assertNotNull(questions, "List should not be null"); + assertEquals(2, questions.size(), "List should contain 2 elements"); + + verify(mockQuestion1, times(1)).copy(); + verify(mockQuestion2, times(1)).copy(); + } + + @Test + void getQuestions_shouldReturnNewList() { + Memento memento = new Memento(originalList); + + List firstCall = memento.getQuestions(); + List secondCall = memento.getQuestions(); + + assertNotSame(firstCall, secondCall, "Each call should return a new list"); + assertEquals(firstCall, secondCall, "But contents should be equal"); + } + + @Test + void constructor_shouldThrowExceptionWhenNullPassed() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new Memento(null); + }); + + assertEquals("Questions cannot be null", exception.getMessage()); + } +} diff --git a/src/test/java/quiz/history/QuestionsHistoryManagerTest.java b/src/test/java/quiz/history/QuestionsHistoryManagerTest.java new file mode 100644 index 0000000..235eb93 --- /dev/null +++ b/src/test/java/quiz/history/QuestionsHistoryManagerTest.java @@ -0,0 +1,104 @@ +package quiz.history; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import quiz.scenarios.Question; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class QuestionsHistoryManagerTest { + + private Question mockQuestion1; + private Question mockQuestion2; + + private List questionsList; + private QuestionsHistoryManager manager; + + @BeforeEach + void setUp() { + mockQuestion1 = mock(Question.class); + mockQuestion2 = mock(Question.class); + + when(mockQuestion1.copy()).thenReturn(mockQuestion1); + when(mockQuestion2.copy()).thenReturn(mockQuestion2); + + questionsList = new ArrayList<>(); + questionsList.add(mockQuestion1); + questionsList.add(mockQuestion2); + + manager = new QuestionsHistoryManager(questionsList); + } + + @Test + void testConstructorWithQuestions_shouldInitializeCorrectly() { + List result = manager.getAllQuestions(); + assertEquals(2, result.size()); + assertTrue(result.contains(mockQuestion1)); + assertTrue(result.contains(mockQuestion2)); + } + + @Test + void testReplaceObjects_shouldReplaceQuestions() { + Question mockNew = mock(Question.class); + when(mockNew.copy()).thenReturn(mockNew); + List newList = List.of(mockNew); + + manager.replaceObjects(newList); + + List result = manager.getAllQuestions(); + assertEquals(1, result.size()); + assertTrue(result.contains(mockNew)); + } + + @Test + void testReplaceObjects_withNull_shouldThrowException() { + Exception ex = assertThrows(IllegalArgumentException.class, () -> { + manager.replaceObjects(null); + }); + + assertEquals("List Questions cannot be null or empty", ex.getMessage()); + } + + @Test + void testReplaceObjects_withEmptyList_shouldThrowException() { + Exception ex = assertThrows(IllegalArgumentException.class, () -> { + manager.replaceObjects(new ArrayList<>()); + }); + + assertEquals("List Questions cannot be null or empty", ex.getMessage()); + } + + @Test + void testBackupAndRestore_shouldRestorePreviousState() { + manager.backup(); + + Question mockNew = mock(Question.class); + when(mockNew.copy()).thenReturn(mockNew); + manager.replaceObjects(List.of(mockNew)); + + assertEquals(1, manager.getAllQuestions().size()); + assertTrue(manager.getAllQuestions().contains(mockNew)); + + manager.restore(); + + List restored = manager.getAllQuestions(); + assertEquals(2, restored.size()); + assertTrue(restored.contains(mockQuestion1)); + assertTrue(restored.contains(mockQuestion2)); + } + + @Test + void testRestore_withoutBackup_shouldDoNothing() { + QuestionsHistoryManager freshManager = new QuestionsHistoryManager(questionsList); + freshManager.restore(); + + List current = freshManager.getAllQuestions(); + assertEquals(2, current.size()); + assertTrue(current.contains(mockQuestion1)); + assertTrue(current.contains(mockQuestion2)); + } +} diff --git a/src/test/java/quiz/scenarios/CheckTest.java b/src/test/java/quiz/scenarios/CheckTest.java new file mode 100644 index 0000000..01c06c8 --- /dev/null +++ b/src/test/java/quiz/scenarios/CheckTest.java @@ -0,0 +1,99 @@ +package quiz.scenarios; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class CheckTest { + + private Check validCheck; + private Check anotherValidCheck; + + @BeforeEach + void setUp() { + validCheck = new Check("Yes", true); + anotherValidCheck = new Check("No", false); + } + + @Test + void testConstructor_shouldThrowExceptionForNullChoice() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new Check(null, true); + }); + + assertEquals("Choice text cannot be null or empty", exception.getMessage(), "Should throw exception when choice is null"); + } + + @Test + void testConstructor_shouldThrowExceptionForEmptyChoice() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new Check("", true); + }); + + assertEquals("Choice text cannot be null or empty", exception.getMessage(), "Should throw exception when choice is empty"); + } + + @Test + void testConstructor_shouldNotThrowExceptionForValidChoice() { + assertDoesNotThrow(() -> new Check("Yes", true), "Constructor should not throw exception for valid choice"); + } + + @Test + void testCopy_shouldReturnDifferentInstanceWithSameValues() { + Check copiedCheck = validCheck.copy(); + + assertNotSame(validCheck, copiedCheck, "The copied check should be a different instance"); + assertEquals(validCheck, copiedCheck, "The copied check should have the same values"); + } + + @Test + void testGetChoice_shouldReturnCorrectChoice() { + assertEquals("Yes", validCheck.getChoice(), "getChoice should return the correct choice"); + } + + @Test + void testIsValidChoice_shouldReturnCorrectValue() { + assertTrue(validCheck.isValidChoice(), "isValidChoice should return true for valid choice"); + assertFalse(anotherValidCheck.isValidChoice(), "isValidChoice should return false for invalid choice"); + } + + @Test + void testGetSelected_shouldReturnDefaultFalse() { + assertFalse(validCheck.getSelected(), "getSelected should return false by default"); + } + + @Test + void testSetSelected_shouldUpdateSelectedStatus() { + validCheck.setSelected(true); + assertTrue(validCheck.getSelected(), "setSelected should correctly update the selected status"); + } + + @Test + void testEquals_shouldReturnTrueForEqualObjects() { + Check anotherCheck = new Check("Yes", true); + assertEquals(validCheck, anotherCheck, "Two Check objects with the same choice and validity should be equal"); + } + + @Test + void testEquals_shouldReturnFalseForDifferentObjects() { + assertNotEquals(validCheck, anotherValidCheck, "Two Check objects with different choices should not be equal"); + } + + @Test + void testHashCode_shouldReturnSameHashCodeForEqualObjects() { + Check anotherCheck = new Check("Yes", true); + assertEquals(validCheck.hashCode(), anotherCheck.hashCode(), "Hash codes should be the same for equal Check objects"); + } + + @Test + void testHashCode_shouldReturnDifferentHashCodeForDifferentObjects() { + assertNotEquals(validCheck.hashCode(), anotherValidCheck.hashCode(), "Hash codes should be different for Check objects with different values"); + } + + @Test + void testToString_shouldReturnCorrectString() { + String expectedString = "Check{choice='Yes', isValidChoice=true, isSelected=false}"; + assertEquals(expectedString, validCheck.toString(), "toString should return the correct string representation"); + } +} diff --git a/src/test/java/quiz/scenarios/QuestionTest.java b/src/test/java/quiz/scenarios/QuestionTest.java new file mode 100644 index 0000000..55cc449 --- /dev/null +++ b/src/test/java/quiz/scenarios/QuestionTest.java @@ -0,0 +1,99 @@ +package quiz.scenarios; + +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +class QuestionTest { + + private Check createCheck(String text, boolean isValid) { + return new Check(text, isValid); + } + + @Test + void testConstructorWithValidData() { + Set checks = Set.of( + createCheck("A", true), + createCheck("B", false) + ); + + Question q = new Question("Q1", "What is the capital?", checks); + + assertEquals("Q1", q.getQuestionID()); + assertEquals("What is the capital?", q.getDescription()); + assertEquals(2, q.getChecks().size()); + } + + @Test + void testConstructorWithNullCheckSetReturnsEmptySet() { + Question q = new Question("Q2", "Some question", null); + assertTrue(q.getChecks().isEmpty()); + } + + @Test + void testConstructorThrowsExceptionOnNullElementInCheckSet() { + Set checks = new HashSet<>(); + checks.add(null); + + assertThrows(IllegalArgumentException.class, () -> + new Question("Q3", "Bad question", checks) + ); + } + + @Test + void testCopyCreatesDeepCopy() { + Check originalCheck = createCheck("A", true); + originalCheck.setSelected(true); + + Set originalChecks = Set.of(originalCheck); + Question original = new Question("Q4", "Copy me", originalChecks); + + Question copy = original.copy(); + + assertEquals(original, copy); + assertNotSame(original, copy); + + Check copiedCheck = copy.getChecks().get(0); + assertNotSame(originalCheck, copiedCheck); + assertEquals(originalCheck, copiedCheck); + } + + @Test + void testGetShuffleChecksReturnsShuffledList() { + Set checks = new LinkedHashSet<>(); + checks.add(createCheck("A", true)); + checks.add(createCheck("B", false)); + checks.add(createCheck("C", false)); + + Question q = new Question("Q5", "Shuffled?", checks); + List original = q.getChecks(); + List shuffled = q.getShuffleChecks(); + + assertEquals(new HashSet<>(original), new HashSet<>(shuffled)); + } + + @Test + void testEqualsAndHashCode() { + Set checks1 = Set.of(createCheck("A", true)); + Set checks2 = Set.of(createCheck("A", true)); + + Question q1 = new Question("Q6", "Same?", checks1); + Question q2 = new Question("Q6", "Same?", checks2); + + assertEquals(q1, q2); + assertEquals(q1.hashCode(), q2.hashCode()); + } + + @Test + void testToStringContainsKeyParts() { + Set checks = Set.of(createCheck("Yes", true)); + Question question = new Question("Q7", "Yes?", checks); + + String result = question.toString(); + assertTrue(result.contains("Q7")); + assertTrue(result.contains("Yes?")); + assertTrue(result.contains("Yes")); + } +} \ No newline at end of file diff --git a/src/test/java/quiz/scenarios/QuestionsTemplateTest.java b/src/test/java/quiz/scenarios/QuestionsTemplateTest.java new file mode 100644 index 0000000..9b2d9f8 --- /dev/null +++ b/src/test/java/quiz/scenarios/QuestionsTemplateTest.java @@ -0,0 +1,100 @@ +package quiz.scenarios; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class QuestionsTemplateTest { + + private Set mockQuestions; + private QuestionsTemplate questionsTemplate; + + @BeforeEach + void setUp() { + mockQuestions = new HashSet<>(); + Question mockQuestion = mock(Question.class); + when(mockQuestion.copy()).thenReturn(mockQuestion); + mockQuestions.add(mockQuestion); + + questionsTemplate = new QuestionsTemplate(mockQuestions); + } + + @Test + void testConstructor_shouldInitializeQuestions() { + assertNotNull(questionsTemplate); + assertEquals(1, questionsTemplate.size(), "Questions set should contain one question"); + } + + @Test + void testSize_shouldReturnCorrectSize() { + assertEquals(1, questionsTemplate.size(), "The size of the questions should be 1"); + } + + @Test + void testGetQuestions_shouldReturnListOfQuestions() { + List questions = questionsTemplate.getQuestions(); + assertNotNull(questions, "Questions list should not be null"); + assertEquals(1, questions.size(), "The list should contain exactly one question"); + } + + @Test + void testGetShuffleQuestions_shouldShuffleQuestions() { + List originalList = questionsTemplate.getQuestions(); + List shuffledList = questionsTemplate.getShuffleQuestions(); + + assertEquals(new HashSet<>(originalList), new HashSet<>(shuffledList), "Shuffled should contain same questions"); + } + + @Test + void testEquals_shouldReturnTrueForEqualObjects() { + QuestionsTemplate copyTemplate = new QuestionsTemplate(mockQuestions); + assertEquals(questionsTemplate, copyTemplate, "The templates should be equal"); + } + + @Test + void testEquals_shouldReturnFalseForDifferentObjects() { + Set differentQuestions = new HashSet<>(); + Question anotherMock = mock(Question.class); + when(anotherMock.copy()).thenReturn(anotherMock); + differentQuestions.add(anotherMock); + + QuestionsTemplate differentTemplate = new QuestionsTemplate(differentQuestions); + assertNotEquals(questionsTemplate, differentTemplate, "The templates should not be equal"); + } + + @Test + void testHashCode_shouldReturnSameHashCodeForEqualObjects() { + QuestionsTemplate copyTemplate = new QuestionsTemplate(mockQuestions); + assertEquals(questionsTemplate.hashCode(), copyTemplate.hashCode(), "Hash codes should match for equal objects"); + } + + @Test + void testCopy_shouldReturnNewInstanceWithSameQuestions() { + QuestionsTemplate copiedTemplate = questionsTemplate.copy(); + assertNotSame(questionsTemplate, copiedTemplate, "Copy should be a different instance"); + assertEquals(questionsTemplate, copiedTemplate, "Copy should be equal in content"); + } + + @Test + void testConstructor_shouldHandleNullQuestions() { + QuestionsTemplate nullQuestionsTemplate = new QuestionsTemplate(null); + assertNotNull(nullQuestionsTemplate, "Template should not be null"); + assertEquals(0, nullQuestionsTemplate.size(), "Size should be 0 for null input"); + } + + @Test + void testConstructor_shouldHandleNullQuestionInSet() { + Set invalidQuestions = new HashSet<>(); + invalidQuestions.add(null); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new QuestionsTemplate(invalidQuestions); + }); + + assertEquals("Question value cannot be null", exception.getMessage(), "Should throw exception for null question"); + } +} \ No newline at end of file