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(){
+
+ // }
+}