From 2505fce920e05cfea9f8dafd98f4e65b04a15b2a Mon Sep 17 00:00:00 2001 From: Ninjaws Date: Wed, 30 Oct 2024 21:47:45 +0100 Subject: [PATCH] [Added] Tests and some more functionality to the Services --- Reflection.md | 14 +++ backend/.devcontainer/devcontainer.json | 5 +- backend/pom.xml | 18 ++- .../quiz/Controllers/QuizController.java | 37 ------ .../java/com/ninjaws/quiz/Models/Request.java | 14 --- .../com/ninjaws/quiz/Services/ApiService.java | 23 ---- .../quiz/{Config => config}/AppConfig.java | 2 +- .../quiz/controllers/QuizController.java | 44 +++++++ .../GlobalExceptionHandler.java | 2 +- .../java/com/ninjaws/quiz/models/Answers.java | 17 +++ .../quiz/{Models => models}/ApiResponse.java | 2 +- .../quiz/{Models => models}/Question.java | 16 +-- .../com/ninjaws/quiz/models/QuizSettings.java | 86 +++++++++++++ .../java/com/ninjaws/quiz/models/Request.java | 14 +++ .../quiz/{Models => models}/Session.java | 2 +- .../com/ninjaws/quiz/services/ApiService.java | 74 ++++++++++++ .../{Services => services}/QuizService.java | 71 ++++++++--- .../ninjaws/quiz/QuizApplicationTests.java | 7 +- .../quiz/scala/QuestionStatusSimulation.scala | 21 ++++ .../ninjaws/quiz/services/ApiServiceTest.java | 114 ++++++++++++++++++ .../quiz/services/QuizServiceTest.java | 73 +++++++++++ 21 files changed, 535 insertions(+), 121 deletions(-) create mode 100644 Reflection.md delete mode 100644 backend/src/main/java/com/ninjaws/quiz/Controllers/QuizController.java delete mode 100644 backend/src/main/java/com/ninjaws/quiz/Models/Request.java delete mode 100644 backend/src/main/java/com/ninjaws/quiz/Services/ApiService.java rename backend/src/main/java/com/ninjaws/quiz/{Config => config}/AppConfig.java (90%) create mode 100644 backend/src/main/java/com/ninjaws/quiz/controllers/QuizController.java rename backend/src/main/java/com/ninjaws/quiz/{Exception => exception}/GlobalExceptionHandler.java (94%) create mode 100644 backend/src/main/java/com/ninjaws/quiz/models/Answers.java rename backend/src/main/java/com/ninjaws/quiz/{Models => models}/ApiResponse.java (91%) rename backend/src/main/java/com/ninjaws/quiz/{Models => models}/Question.java (83%) create mode 100644 backend/src/main/java/com/ninjaws/quiz/models/QuizSettings.java create mode 100644 backend/src/main/java/com/ninjaws/quiz/models/Request.java rename backend/src/main/java/com/ninjaws/quiz/{Models => models}/Session.java (89%) create mode 100644 backend/src/main/java/com/ninjaws/quiz/services/ApiService.java rename backend/src/main/java/com/ninjaws/quiz/{Services => services}/QuizService.java (61%) create mode 100644 backend/src/test/java/com/ninjaws/quiz/scala/QuestionStatusSimulation.scala create mode 100644 backend/src/test/java/com/ninjaws/quiz/services/ApiServiceTest.java create mode 100644 backend/src/test/java/com/ninjaws/quiz/services/QuizServiceTest.java diff --git a/Reflection.md b/Reflection.md new file mode 100644 index 0000000..21a6bd3 --- /dev/null +++ b/Reflection.md @@ -0,0 +1,14 @@ +# Issues + +## Issues during testing +### Scheduler +I don't know if it was the scheduler specifically, but whenever I try to test it, both the cache and the queue seem to become empty after the first function is completed. +I don't know how to solve this yet + + +## Caching solution +### Lack of data +Not being able to gather blocks of 50 is a huge issue. The idea of reusing blocks becomes less viable if I can just get a big chunk every time for others to use +I can't force a big chunk, because it might just return a status 1, and then the user gets nothing. + +Perhaps I can separate the questions myself and make a collection, that i can build the requests from. However, for every quest tracking if someone has already seen is a huge pain. diff --git a/backend/.devcontainer/devcontainer.json b/backend/.devcontainer/devcontainer.json index aead60f..22891ae 100644 --- a/backend/.devcontainer/devcontainer.json +++ b/backend/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "context": "..", "dockerfile": "../Dockerfile", "args": { - "WORKDIR": "/app", + "WORKDIR": "/app" // "UID": "${localEnv:USER_UID}", // "GID": "${localEnv:USER_GID}" }, @@ -24,7 +24,8 @@ "settings": {}, "extensions": [ "vscjava.vscode-java-pack", - "Oracle.oracle-java" + "Oracle.oracle-java", + "scala-lang.scala" ] } } diff --git a/backend/pom.xml b/backend/pom.xml index 47ec3a2..ac725c2 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -28,12 +28,14 @@ 17 + 1.18.34 + 3.1.8 - + org.springframework.boot spring-boot-starter-web @@ -45,11 +47,11 @@ runtime true - + org.springframework.boot spring-boot-starter-test @@ -58,14 +60,18 @@ com.github.ben-manes.caffeine caffeine - 3.1.8 + ${caffeine.version} org.projectlombok lombok - 1.18.34 + ${lombok.version} provided + + org.springframework.boot + spring-boot-starter-validation + diff --git a/backend/src/main/java/com/ninjaws/quiz/Controllers/QuizController.java b/backend/src/main/java/com/ninjaws/quiz/Controllers/QuizController.java deleted file mode 100644 index 6ab69c3..0000000 --- a/backend/src/main/java/com/ninjaws/quiz/Controllers/QuizController.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.ninjaws.quiz.Controllers; - -import java.util.Optional; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.ninjaws.quiz.Models.Session; -import com.ninjaws.quiz.Services.QuizService; - -@RestController -@RequestMapping("/quiz") -public class QuizController { - - @Autowired - private QuizService quizService; - - @GetMapping("/questions") - public String startSession() { - // String sessionId = generateSessionId(); - String sessionId = quizService.createSession(); - return sessionId; - } - - @GetMapping("/questions/{sessionId}") - public Optional checkStatus(@PathVariable String sessionId) { - return quizService.checkSession(sessionId); - } - - // @PostMapping("/checkanswers") - // public Result checkAnswers(@RequestBody String answers) { - // return new Result(); - // } -} diff --git a/backend/src/main/java/com/ninjaws/quiz/Models/Request.java b/backend/src/main/java/com/ninjaws/quiz/Models/Request.java deleted file mode 100644 index 15205ef..0000000 --- a/backend/src/main/java/com/ninjaws/quiz/Models/Request.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.ninjaws.quiz.Models; - -import lombok.Getter; - -@Getter -public class Request { - private final String sessionId; - private final String requestUrl; - - public Request(String sessionId, String requestUrl) { - this.sessionId = sessionId; - this.requestUrl = requestUrl; - } -} diff --git a/backend/src/main/java/com/ninjaws/quiz/Services/ApiService.java b/backend/src/main/java/com/ninjaws/quiz/Services/ApiService.java deleted file mode 100644 index 189f4b2..0000000 --- a/backend/src/main/java/com/ninjaws/quiz/Services/ApiService.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.ninjaws.quiz.Services; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -@Service -public class ApiService { - - private final RestTemplate restTemplate; - - @Autowired - public ApiService(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - public String requestQuestions(String url) { - ResponseEntity response = restTemplate.getForEntity(url, String.class); - String jsonData = response.getBody(); - return jsonData; - } -} diff --git a/backend/src/main/java/com/ninjaws/quiz/Config/AppConfig.java b/backend/src/main/java/com/ninjaws/quiz/config/AppConfig.java similarity index 90% rename from backend/src/main/java/com/ninjaws/quiz/Config/AppConfig.java rename to backend/src/main/java/com/ninjaws/quiz/config/AppConfig.java index a426b14..eef023d 100644 --- a/backend/src/main/java/com/ninjaws/quiz/Config/AppConfig.java +++ b/backend/src/main/java/com/ninjaws/quiz/config/AppConfig.java @@ -1,4 +1,4 @@ -package com.ninjaws.quiz.Config; +package com.ninjaws.quiz.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/backend/src/main/java/com/ninjaws/quiz/controllers/QuizController.java b/backend/src/main/java/com/ninjaws/quiz/controllers/QuizController.java new file mode 100644 index 0000000..d1cbe81 --- /dev/null +++ b/backend/src/main/java/com/ninjaws/quiz/controllers/QuizController.java @@ -0,0 +1,44 @@ +package com.ninjaws.quiz.controllers; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.ninjaws.quiz.models.Answers; +import com.ninjaws.quiz.models.Session; +import com.ninjaws.quiz.services.QuizService; + +@RestController +@RequestMapping("/quiz") +public class QuizController { + + @Autowired + private QuizService quizService; + + @GetMapping("/questions") + public String startSession( + @RequestParam(required = true) String amount, + @RequestParam(required = false) Integer category, + @RequestParam(required = false) String difficulty, + @RequestParam(required = false) String type + ) { + return quizService.createSession(quizService.jsonToQuizSettings("todo")); + } + + @GetMapping("/status/{sessionId}") + public Optional checkStatus(@PathVariable String sessionId) { + return quizService.checkSession(sessionId); + } + + @PostMapping("/checkanswers") + public String checkAnswers(@RequestBody Answers answers) { + return quizService.getScore(answers); + } +} diff --git a/backend/src/main/java/com/ninjaws/quiz/Exception/GlobalExceptionHandler.java b/backend/src/main/java/com/ninjaws/quiz/exception/GlobalExceptionHandler.java similarity index 94% rename from backend/src/main/java/com/ninjaws/quiz/Exception/GlobalExceptionHandler.java rename to backend/src/main/java/com/ninjaws/quiz/exception/GlobalExceptionHandler.java index 07d7a06..c684ebc 100644 --- a/backend/src/main/java/com/ninjaws/quiz/Exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/ninjaws/quiz/exception/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package com.ninjaws.quiz.Exception; +package com.ninjaws.quiz.exception; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/backend/src/main/java/com/ninjaws/quiz/models/Answers.java b/backend/src/main/java/com/ninjaws/quiz/models/Answers.java new file mode 100644 index 0000000..c87ee61 --- /dev/null +++ b/backend/src/main/java/com/ninjaws/quiz/models/Answers.java @@ -0,0 +1,17 @@ +package com.ninjaws.quiz.models; + +import java.util.List; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Answers { + private final String sessionId; + private List answers; + + public Answers(String sessionId) { + this.sessionId = sessionId; + } +} diff --git a/backend/src/main/java/com/ninjaws/quiz/Models/ApiResponse.java b/backend/src/main/java/com/ninjaws/quiz/models/ApiResponse.java similarity index 91% rename from backend/src/main/java/com/ninjaws/quiz/Models/ApiResponse.java rename to backend/src/main/java/com/ninjaws/quiz/models/ApiResponse.java index 0d1da5c..776d0e0 100644 --- a/backend/src/main/java/com/ninjaws/quiz/Models/ApiResponse.java +++ b/backend/src/main/java/com/ninjaws/quiz/models/ApiResponse.java @@ -1,4 +1,4 @@ -package com.ninjaws.quiz.Models; +package com.ninjaws.quiz.models; import java.util.List; diff --git a/backend/src/main/java/com/ninjaws/quiz/Models/Question.java b/backend/src/main/java/com/ninjaws/quiz/models/Question.java similarity index 83% rename from backend/src/main/java/com/ninjaws/quiz/Models/Question.java rename to backend/src/main/java/com/ninjaws/quiz/models/Question.java index 4efb9a9..36ee786 100644 --- a/backend/src/main/java/com/ninjaws/quiz/Models/Question.java +++ b/backend/src/main/java/com/ninjaws/quiz/models/Question.java @@ -1,4 +1,4 @@ -package com.ninjaws.quiz.Models; +package com.ninjaws.quiz.models; import java.util.ArrayList; import java.util.Collections; @@ -9,22 +9,16 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; -/** - * Holds the question, answers, and correct answer - * TODO: Should not send correct answer to the user - */ -// @Setter() -@Getter() +@Getter public class Question { private String type; private String difficulty; private String category; private String question; + - // @JsonIgnore - private String correctAnswer; - - // @JsonIgnore + //* Hidden from the user */ + private String correctAnswer; private List incorrectAnswers; @JsonProperty("answers") diff --git a/backend/src/main/java/com/ninjaws/quiz/models/QuizSettings.java b/backend/src/main/java/com/ninjaws/quiz/models/QuizSettings.java new file mode 100644 index 0000000..5bca555 --- /dev/null +++ b/backend/src/main/java/com/ninjaws/quiz/models/QuizSettings.java @@ -0,0 +1,86 @@ +package com.ninjaws.quiz.models; + +import com.ninjaws.quiz.models.QuizSettings.Difficulty; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class QuizSettings { + + public enum Type { + ANY, BOOLEAN, MULTIPLE; + + public String getApiValue() { + return this.name().toLowerCase(); + } + } + + public enum Difficulty { + ANY, EASY, MEDIUM, HARD; + + public String getApiValue() { + return this.name().toLowerCase(); + } + } + + public enum Category { + ANY, GEN_KNOWLEDGE, ENT_BOOKS, ENT_FILM, ENT_MUSIC, ENT_MUSICAL_THEATRE, ENT_TELEVISION, + ENT_VIDEOGAMES, ENT_BOARDGAMES, SCIENCE_NATURE, SCIENCE_COMPUTERS, SCIENCE_MATH, MYTHOLOGY, SPORTS, GEOGRAPHY, HISTORY, POLITICS, ART, CELEBRITIES, + ANIMALS, VEHICLES, ENT_COMICS, SCIENCE_GADGETS, ENT_ANIME_MANGA, ENT_CARTOON_ANIMATIONS; + + /** + * For some reason the external API starts with a value of 9 for the General Knowledge category, so we have to adjust + * ANY also gets adjusted, but it doesn't matter since that one doesn't get used as parameter anyway + * @return The corrected value + */ + public int getApiValue() { + return 8 + this.ordinal(); + } + } + + @Min(1) + @Max(50) + private int amount = 10; + + private Type type = Type.ANY; + + private Difficulty difficulty = Difficulty.ANY; + + private Category category = Category.ANY; + + + /** + * Ensures the value is between 1 and 50 + * @Valid doesn't seem to be enforcing it, so this ensures the value is legal + */ + public void setAmount(@Valid int amount) { + this.amount = Math.max(1, Math.min(amount, 50)); + } + + /** + * Converts the class to a format that can be provided as arguments for the api request + * @return The resulting request arguments as String + */ + public String toApiQuery() { + StringBuilder query = new StringBuilder("?amount=" + amount); + + if (category != null && category != Category.ANY) { + query.append("&category=").append(category.getApiValue()); + } + + if (difficulty != null && difficulty != Difficulty.ANY) { + query.append("&difficulty=").append(difficulty.getApiValue()); + } + + if (type != null && type != Type.ANY) { + query.append("&type=").append(type.getApiValue()); + } + + return query.toString(); + } +} diff --git a/backend/src/main/java/com/ninjaws/quiz/models/Request.java b/backend/src/main/java/com/ninjaws/quiz/models/Request.java new file mode 100644 index 0000000..d04c3bf --- /dev/null +++ b/backend/src/main/java/com/ninjaws/quiz/models/Request.java @@ -0,0 +1,14 @@ +package com.ninjaws.quiz.models; + +import lombok.Getter; + +@Getter +public class Request { + private final String sessionId; + private final QuizSettings quizSettings; + + public Request(String sessionId, QuizSettings quizSettings) { + this.sessionId = sessionId; + this.quizSettings = quizSettings; + } +} diff --git a/backend/src/main/java/com/ninjaws/quiz/Models/Session.java b/backend/src/main/java/com/ninjaws/quiz/models/Session.java similarity index 89% rename from backend/src/main/java/com/ninjaws/quiz/Models/Session.java rename to backend/src/main/java/com/ninjaws/quiz/models/Session.java index f0f8ab5..540d756 100644 --- a/backend/src/main/java/com/ninjaws/quiz/Models/Session.java +++ b/backend/src/main/java/com/ninjaws/quiz/models/Session.java @@ -1,4 +1,4 @@ -package com.ninjaws.quiz.Models; +package com.ninjaws.quiz.models; import java.util.List; import java.util.UUID; diff --git a/backend/src/main/java/com/ninjaws/quiz/services/ApiService.java b/backend/src/main/java/com/ninjaws/quiz/services/ApiService.java new file mode 100644 index 0000000..4201d7a --- /dev/null +++ b/backend/src/main/java/com/ninjaws/quiz/services/ApiService.java @@ -0,0 +1,74 @@ +package com.ninjaws.quiz.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.ninjaws.quiz.models.QuizSettings; + +import jakarta.validation.Valid; + +@Service +public class ApiService { + + // @Autowired + // private ObjectMapper objectMapper; + private final RestTemplate restTemplate; + + private final String apiUrl = "https://opentdb.com/api.php"; + + // private String sessionToken = ""; + + @Autowired + public ApiService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + // try { + // sessionToken = requestSessionToken(); + // } catch (Exception e) { + // // Failed to get a session token, setting default to none + // sessionToken = ""; + + // } + } + + private String buildRequestString(QuizSettings quizSettings) { + return apiUrl + quizSettings.toApiQuery(); + } + + // private int getResponseCode(String json) throws Exception { + // JsonNode jsonNode = objectMapper.readTree(json); + // return jsonNode.get("response_code").asInt(); + // } + + // public String requestSessionToken() throws Exception { + // String sessionTokenRequestUrl = "https://opentdb.com/api_token.php?command=request"; + // ResponseEntity response = restTemplate.getForEntity(sessionTokenRequestUrl, String.class); + // JsonNode jsonNode = objectMapper.readTree(response.getBody()); + // if (jsonNode.get("response_code").asInt() == 0) { + // sessionToken = jsonNode.get("token").asText(); + // }else { + // // Failed to get a session token, setting default to none + // sessionToken = ""; + // } + // return sessionToken; + // } + + public String requestQuestions(@Valid QuizSettings quizSettings) { + String url = buildRequestString(quizSettings); + ResponseEntity response = restTemplate.getForEntity(url, String.class); + String jsonData = response.getBody(); + + // try { + // int responseCode = getResponseCode(jsonData); + // /** When the code has expired or you've ran out of questions, update the token and try again */ + // if(responseCode == 3 || responseCode == 4) { + // requestSessionToken(); + // requestQuestions(quizSettings); + // } + // } catch (Exception e) { + // // Failed to get a responseCode, meaning the request failed completely (they always send a response_code) + // } + return jsonData; + } +} diff --git a/backend/src/main/java/com/ninjaws/quiz/Services/QuizService.java b/backend/src/main/java/com/ninjaws/quiz/services/QuizService.java similarity index 61% rename from backend/src/main/java/com/ninjaws/quiz/Services/QuizService.java rename to backend/src/main/java/com/ninjaws/quiz/services/QuizService.java index 53924fc..8a30239 100644 --- a/backend/src/main/java/com/ninjaws/quiz/Services/QuizService.java +++ b/backend/src/main/java/com/ninjaws/quiz/services/QuizService.java @@ -1,22 +1,25 @@ -package com.ninjaws.quiz.Services; +package com.ninjaws.quiz.services; import java.io.IOException; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import com.fasterxml.jackson.databind.ObjectMapper; -import com.ninjaws.quiz.Models.ApiResponse; -import com.ninjaws.quiz.Models.Question; -import com.ninjaws.quiz.Models.Request; -import com.ninjaws.quiz.Models.Session; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.ninjaws.quiz.models.Answers; +import com.ninjaws.quiz.models.ApiResponse; +import com.ninjaws.quiz.models.Question; +import com.ninjaws.quiz.models.QuizSettings; +import com.ninjaws.quiz.models.Request; +import com.ninjaws.quiz.models.Session; @Service @@ -31,9 +34,9 @@ public class QuizService { * Stores all active sessions * Gets added to when a user requests data * Gets removed from when a user requests their score (by sending their answers) - * TODO: Replace with something that automatically removes items after a certain amount of time (Caffein?) */ - private final Map sessions = new ConcurrentHashMap<>(); + // private final Map sessions = new ConcurrentHashMap<>(); + private final Cache sessions; /** * Stores all user requests for data @@ -42,18 +45,34 @@ public class QuizService { */ private final BlockingQueue requests = new LinkedBlockingQueue<>(); + + public QuizService() { + sessions = (Caffeine.newBuilder() + .expireAfterWrite(1,TimeUnit.HOURS) + .build()); + } + + public QuizService(Cache cache) { + sessions = cache; + } + + public QuizSettings jsonToQuizSettings(String json){ + QuizSettings quizSettings = new QuizSettings(); + return quizSettings; + } + /** * Actions: * - Creates a session with sessionId * - Adds session to the cache - * - Creates request and adds it to the queue + * - Creates a request and adds it to the queue + * @param quizSettings The values set by the user, determining the kind of quiz they want * @return The sessionId */ - public String createSession() { + public String createSession(QuizSettings quizSettings) { Session session = new Session(); - sessions.put(session.getId(), session); // TODO: Either remove id from session, or maybe Caffein does allow them together try { - requests.put(new Request(session.getId(), "https://opentdb.com/api.php?amount=10")); + requests.put(new Request(session.getId(), quizSettings)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -62,14 +81,14 @@ public class QuizService { } /** - * @param sessionId * Actions: * - Looks up the session in the cache * - Checks if the data from the request is stored - * @return The data if it's available, otherwise a statuscode (?) + * @param sessionId + * @return The data if it's available, Optional.empty() if not, and an Exception if the session is invalid */ public Optional checkSession(String sessionId) throws IllegalArgumentException { - Session session = sessions.get(sessionId); + Session session = sessions.getIfPresent(sessionId); if(session == null) { throw new IllegalArgumentException("Invalid session ID: " + sessionId); @@ -92,12 +111,15 @@ public class QuizService { */ @Scheduled(fixedDelay = 5000) protected void processQueue() { + System.out.print("Schedule!"); Request request = requests.poll(); if (request != null) { - String json = apiService.requestQuestions(request.getRequestUrl()); + System.out.print("----------------An item!"); + String json = apiService.requestQuestions(request.getQuizSettings()); + System.out.print(json); try { List questions = extractQuestions(json); - Session session = sessions.get(request.getSessionId()); + Session session = sessions.getIfPresent(request.getSessionId()); session.setQuestions(questions); sessions.put(request.getSessionId(), session); } catch (IOException e) { @@ -112,6 +134,7 @@ public class QuizService { /** * Receives: sessionId and answers + * * Actions: * - retrieves session from cache * - Deletes session from cache @@ -119,8 +142,16 @@ public class QuizService { * - Calculates score * Returns: calculated score * */ - public void calculateScore() { - + public String getScore(Answers answers) { + Session session = sessions.getIfPresent(answers.getSessionId()); + sessions.invalidate(session.getId()); + return calculateScore(session, answers); + } + + private String calculateScore(Session session, Answers answers) { + System.out.print(session); + System.out.print(answers); + return "You did great!"; } } diff --git a/backend/src/test/java/com/ninjaws/quiz/QuizApplicationTests.java b/backend/src/test/java/com/ninjaws/quiz/QuizApplicationTests.java index 5958ed5..bac272a 100644 --- a/backend/src/test/java/com/ninjaws/quiz/QuizApplicationTests.java +++ b/backend/src/test/java/com/ninjaws/quiz/QuizApplicationTests.java @@ -1,13 +1,12 @@ package com.ninjaws.quiz; -import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class QuizApplicationTests { - @Test - void contextLoads() { - } + // @Test + // void contextLoads() { + // } } diff --git a/backend/src/test/java/com/ninjaws/quiz/scala/QuestionStatusSimulation.scala b/backend/src/test/java/com/ninjaws/quiz/scala/QuestionStatusSimulation.scala new file mode 100644 index 0000000..9f75a56 --- /dev/null +++ b/backend/src/test/java/com/ninjaws/quiz/scala/QuestionStatusSimulation.scala @@ -0,0 +1,21 @@ +package com.ninjaws.quiz.scala; +import io.gatling.core.Predef._ +import io.gatling.http.Predef._ +import scala.concurrent.duration._ + +class QuestionStatusSimulation extends Simulation { + val httpProtocol = http.baseUrl("http://localhost:9090").acceptHeader("application/json") + + val requestQuestions = exec(http("Request Questions").get("/questions/bla").check(status.is(200)).check(jsonPath("$.id").saveAs("sessionId"))) + + val checkStatus = exec(http("Check Status").get("/status/${sessionId}").check(status.is(200),jsonPath("$.status").notNull)) + + val scn = scenario("Check Questions Status Completion").exec(requestQuestions).pause(1).asLongAsDuring(session => session("completed").asOption[Boolean].getOrElse(false) == false, 30.seconds) { + exec(checkStatus).pause(1).exec{ session => val status = session("status").asOption[String] + session.set("completed", status.isDefined && status.get != "null") + } + } + setUp( + scn.inject(atOnceUsers(10)) + ).protocols(httpProtocol); +} \ No newline at end of file diff --git a/backend/src/test/java/com/ninjaws/quiz/services/ApiServiceTest.java b/backend/src/test/java/com/ninjaws/quiz/services/ApiServiceTest.java new file mode 100644 index 0000000..56d112b --- /dev/null +++ b/backend/src/test/java/com/ninjaws/quiz/services/ApiServiceTest.java @@ -0,0 +1,114 @@ +package com.ninjaws.quiz.services; + +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ninjaws.quiz.models.QuizSettings; + +@SpringBootTest +public class ApiServiceTest { + + @Autowired + private ApiService apiService; + + @Autowired + private ObjectMapper objectMapper; + + /** + * The external API only allows one request every 5 seconds + */ + @AfterEach + public void cooldown() throws InterruptedException { + Thread.sleep(5000); + } + + @Test + public void testRequestQuestions() throws Exception { + QuizSettings settings = new QuizSettings(); + String jsonResponse = apiService.requestQuestions(settings); + JsonNode jsonNode = objectMapper.readTree(jsonResponse); + + // Check if the response code is as expected + assertThat(jsonNode.has("response_code")).isTrue(); + assertThat(jsonNode.get("response_code").asInt()).isEqualTo(0); + + // Check the results array + assertThat(jsonNode.has("results")).isTrue(); + assertThat(jsonNode.get("results").isArray()).isTrue(); + assertThat(jsonNode.get("results").size()).isEqualTo(10); + + // Verify the first result has the expected fields + JsonNode firstResult = jsonNode.get("results").get(0); + assertThat(firstResult.has("type")).isTrue(); + assertThat(firstResult.has("difficulty")).isTrue(); + assertThat(firstResult.has("category")).isTrue(); + assertThat(firstResult.has("question")).isTrue(); + assertThat(firstResult.has("correct_answer")).isTrue(); + assertThat(firstResult.has("incorrect_answers")).isTrue(); + assertThat(firstResult.get("incorrect_answers").size()).isGreaterThan(0); + } + + @Test + public void testAllParams() throws Exception { + int amount = 10; + + QuizSettings settings = new QuizSettings(); + settings.setAmount(amount); + settings.setCategory(QuizSettings.Category.HISTORY); + settings.setType(QuizSettings.Type.MULTIPLE); + settings.setDifficulty(QuizSettings.Difficulty.MEDIUM); + + String jsonResponse = apiService.requestQuestions(settings); + JsonNode jsonNode = objectMapper.readTree(jsonResponse); + + // Check if the response code is as expected + assertThat(jsonNode.has("response_code")).isTrue(); + assertThat(jsonNode.get("response_code").asInt()).isEqualTo(0); + + // Check the results array + assertThat(jsonNode.has("results")).isTrue(); + assertThat(jsonNode.get("results").isArray()).isTrue(); + assertThat(jsonNode.get("results").size()).isEqualTo(amount); + + // Verify the first result has the expected fields + JsonNode firstResult = jsonNode.get("results").get(0); + String retrievedType = firstResult.get("type").toString(); + String retrievedDifficulty = firstResult.get("difficulty").toString(); + String retrievedCategory = firstResult.get("category").toString(); + assertThat(retrievedType.equals(settings.getType().getApiValue())); + assertThat(retrievedDifficulty.equals(settings.getDifficulty().getApiValue())); + assertThat(retrievedCategory.equals("Animals")); + assertThat(firstResult.get("incorrect_answers").size()).isEqualTo(3); + } + + /** + * If the API doesn't have the requested amount of questions, it will return an empty list and respond with 1 + */ + @Test + public void testCombinationThatResultsInZeroQuestions() throws Exception { + int amount = 30; + + QuizSettings settings = new QuizSettings(); + settings.setAmount(amount); + settings.setCategory(QuizSettings.Category.ANIMALS); + settings.setType(QuizSettings.Type.MULTIPLE); + settings.setDifficulty(QuizSettings.Difficulty.MEDIUM); + + String jsonResponse = apiService.requestQuestions(settings); + JsonNode jsonNode = objectMapper.readTree(jsonResponse); + + // Check if the response code is as expected + assertThat(jsonNode.has("response_code")).isTrue(); + assertThat(jsonNode.get("response_code").asInt()).isEqualTo(1); + + // Check the results array + assertThat(jsonNode.has("results")).isTrue(); + assertThat(jsonNode.get("results").isArray()).isTrue(); + assertThat(jsonNode.get("results").size()).isEqualTo(0); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/ninjaws/quiz/services/QuizServiceTest.java b/backend/src/test/java/com/ninjaws/quiz/services/QuizServiceTest.java new file mode 100644 index 0000000..bef75fc --- /dev/null +++ b/backend/src/test/java/com/ninjaws/quiz/services/QuizServiceTest.java @@ -0,0 +1,73 @@ +package com.ninjaws.quiz.services; + +import org.junit.jupiter.api.TestInstance; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +// @EnableScheduling +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class QuizServiceTest { + + // @InjectMocks + // private QuizService quizService; + + // @Mock + // private ApiService apiService; + + // @BeforeAll + // public void setUp() { + // MockitoAnnotations.openMocks(this); + // // Cache cache = Caffeine.newBuilder() + // // .expireAfterWrite(1, TimeUnit.HOURS) + // // .build(); // Create a mock cache or a real cache for tests + // // quizService = new QuizService(cache); + // } + + // @Test + // public void testCreateSession() { + // QuizSettings settings = new QuizSettings(); + // String response = quizService.createSession(settings); + // assertTrue(response instanceof String); + // } + + // /** + // * TODO: Fix the test + // * Seems to clear the cache and queue when the createSession is finished + // */ + // @Test + // public void testCheckSession() throws InterruptedException { + + // QuizSettings settings = new QuizSettings(); + // when(apiService.requestQuestions(settings)).thenReturn("{\"response_code\":0,\"results\":[{\"type\":\"multiple\",\"difficulty\":\"hard\",\"category\":\"Entertainment: Video Games\",\"question\":\"The map featured in Arma 3 named "Altis" is based off of what Greek island?\",\"correct_answer\":\"Lemnos\",\"incorrect_answers\":[\"Ithaca\",\"Naxos\",\"Anafi\"]},{\"type\":\"multiple\",\"difficulty\":\"medium\",\"category\":\"Entertainment: Music\",\"question\":\"What is the last song on the first Panic! At the Disco album?\",\"correct_answer\":\"Build God, Then We'll Talk\",\"incorrect_answers\":[\"Nails for Breakfast, Tacks for Snacks\",\"I Write Sins Not Tragedies\",\"Lying Is The Most Fun A Girl Can Have Without Taking Her Clothes Off\"]},{\"type\":\"multiple\",\"difficulty\":\"easy\",\"category\":\"Entertainment: Video Games\",\"question\":\"Which of these is NOT a playable character in the 2016 video game Overwatch?\",\"correct_answer\":\"Invoker\",\"incorrect_answers\":[\"Mercy\",\"Winston\",\"Zenyatta\"]},{\"type\":\"multiple\",\"difficulty\":\"medium\",\"category\":\"Science: Computers\",\"question\":\"What does AD stand for in relation to Windows Operating Systems? \",\"correct_answer\":\"Active Directory\",\"incorrect_answers\":[\"Alternative Drive\",\"Automated Database\",\"Active Department\"]},{\"type\":\"multiple\",\"difficulty\":\"medium\",\"category\":\"Entertainment: Japanese Anime & Manga\",\"question\":\"In "Highschool DxD", Koneko Toujou is from what race?\",\"correct_answer\":\"Nekomata\",\"incorrect_answers\":[\"Kitsune\",\"Human\",\"Kappa\"]}]}"); + // String sessionId = quizService.createSession(settings); + + + // int interval = 1000; + // int maxTime = 5000; + // long startTime = System.currentTimeMillis(); + // Session session = null; + + // while((System.currentTimeMillis() - maxTime) < startTime) { + // Optional retrieved = quizService.checkSession(sessionId); + // System.out.println(retrieved); + // if (retrieved.isPresent()) { + // System.out.print("Present!"); + // session = retrieved.get(); + // break; + // } else { + // try { + // Thread.sleep(interval); + // } catch (InterruptedException e) { + // Thread.currentThread().interrupt(); // Restore interrupted status + // fail("Thread was interrupted during sleep"); + // } + // } + // } + + // assertNotEquals(session, null); + // } + + // public void testCalculateScore(){ + + // } +}