[Added] Tests and some more functionality to the Services
This commit is contained in:
@@ -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.
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"context": "..",
|
"context": "..",
|
||||||
"dockerfile": "../Dockerfile",
|
"dockerfile": "../Dockerfile",
|
||||||
"args": {
|
"args": {
|
||||||
"WORKDIR": "/app",
|
"WORKDIR": "/app"
|
||||||
// "UID": "${localEnv:USER_UID}",
|
// "UID": "${localEnv:USER_UID}",
|
||||||
// "GID": "${localEnv:USER_GID}"
|
// "GID": "${localEnv:USER_GID}"
|
||||||
},
|
},
|
||||||
@@ -24,7 +24,8 @@
|
|||||||
"settings": {},
|
"settings": {},
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"vscjava.vscode-java-pack",
|
"vscjava.vscode-java-pack",
|
||||||
"Oracle.oracle-java"
|
"Oracle.oracle-java",
|
||||||
|
"scala-lang.scala"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-6
@@ -28,12 +28,14 @@
|
|||||||
</scm>
|
</scm>
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>17</java.version>
|
<java.version>17</java.version>
|
||||||
|
<lombok.version>1.18.34</lombok.version>
|
||||||
|
<caffeine.version>3.1.8</caffeine.version>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<!-- <dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
</dependency>
|
</dependency> -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
@@ -45,11 +47,11 @@
|
|||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<!-- <dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency> -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
@@ -58,14 +60,18 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
<artifactId>caffeine</artifactId>
|
<artifactId>caffeine</artifactId>
|
||||||
<version>3.1.8</version>
|
<version>${caffeine.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.34</version>
|
<version>${lombok.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -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<Session> checkStatus(@PathVariable String sessionId) {
|
|
||||||
return quizService.checkSession(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @PostMapping("/checkanswers")
|
|
||||||
// public Result checkAnswers(@RequestBody String answers) {
|
|
||||||
// return new Result();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<String> response = restTemplate.getForEntity(url, String.class);
|
|
||||||
String jsonData = response.getBody();
|
|
||||||
return jsonData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -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<Session> checkStatus(@PathVariable String sessionId) {
|
||||||
|
return quizService.checkSession(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/checkanswers")
|
||||||
|
public String checkAnswers(@RequestBody Answers answers) {
|
||||||
|
return quizService.getScore(answers);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.ninjaws.quiz.Exception;
|
package com.ninjaws.quiz.exception;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -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<String> answers;
|
||||||
|
|
||||||
|
public Answers(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.ninjaws.quiz.Models;
|
package com.ninjaws.quiz.models;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
+5
-11
@@ -1,4 +1,4 @@
|
|||||||
package com.ninjaws.quiz.Models;
|
package com.ninjaws.quiz.models;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -9,22 +9,16 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
/**
|
@Getter
|
||||||
* Holds the question, answers, and correct answer
|
|
||||||
* TODO: Should not send correct answer to the user
|
|
||||||
*/
|
|
||||||
// @Setter()
|
|
||||||
@Getter()
|
|
||||||
public class Question {
|
public class Question {
|
||||||
private String type;
|
private String type;
|
||||||
private String difficulty;
|
private String difficulty;
|
||||||
private String category;
|
private String category;
|
||||||
private String question;
|
private String question;
|
||||||
|
|
||||||
|
|
||||||
// @JsonIgnore
|
//* Hidden from the user */
|
||||||
private String correctAnswer;
|
private String correctAnswer;
|
||||||
|
|
||||||
// @JsonIgnore
|
|
||||||
private List<String> incorrectAnswers;
|
private List<String> incorrectAnswers;
|
||||||
|
|
||||||
@JsonProperty("answers")
|
@JsonProperty("answers")
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.ninjaws.quiz.Models;
|
package com.ninjaws.quiz.models;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -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<String> 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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
-20
@@ -1,22 +1,25 @@
|
|||||||
package com.ninjaws.quiz.Services;
|
package com.ninjaws.quiz.services;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.ninjaws.quiz.Models.ApiResponse;
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
import com.ninjaws.quiz.Models.Question;
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
import com.ninjaws.quiz.Models.Request;
|
import com.ninjaws.quiz.models.Answers;
|
||||||
import com.ninjaws.quiz.Models.Session;
|
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
|
@Service
|
||||||
@@ -31,9 +34,9 @@ public class QuizService {
|
|||||||
* Stores all active sessions
|
* Stores all active sessions
|
||||||
* Gets added to when a user requests data
|
* Gets added to when a user requests data
|
||||||
* Gets removed from when a user requests their score (by sending their answers)
|
* 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<String,Session> sessions = new ConcurrentHashMap<>();
|
// private final Map<String,Session> sessions = new ConcurrentHashMap<>();
|
||||||
|
private final Cache<String,Session> sessions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores all user requests for data
|
* Stores all user requests for data
|
||||||
@@ -42,18 +45,34 @@ public class QuizService {
|
|||||||
*/
|
*/
|
||||||
private final BlockingQueue<Request> requests = new LinkedBlockingQueue<>();
|
private final BlockingQueue<Request> requests = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
|
|
||||||
|
public QuizService() {
|
||||||
|
sessions = (Caffeine.newBuilder()
|
||||||
|
.expireAfterWrite(1,TimeUnit.HOURS)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuizService(Cache<String, Session> cache) {
|
||||||
|
sessions = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuizSettings jsonToQuizSettings(String json){
|
||||||
|
QuizSettings quizSettings = new QuizSettings();
|
||||||
|
return quizSettings;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actions:
|
* Actions:
|
||||||
* - Creates a session with sessionId
|
* - Creates a session with sessionId
|
||||||
* - Adds session to the cache
|
* - 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
|
* @return The sessionId
|
||||||
*/
|
*/
|
||||||
public String createSession() {
|
public String createSession(QuizSettings quizSettings) {
|
||||||
Session session = new Session();
|
Session session = new Session();
|
||||||
sessions.put(session.getId(), session); // TODO: Either remove id from session, or maybe Caffein does allow them together
|
|
||||||
try {
|
try {
|
||||||
requests.put(new Request(session.getId(), "https://opentdb.com/api.php?amount=10"));
|
requests.put(new Request(session.getId(), quizSettings));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
@@ -62,14 +81,14 @@ public class QuizService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param sessionId
|
|
||||||
* Actions:
|
* Actions:
|
||||||
* - Looks up the session in the cache
|
* - Looks up the session in the cache
|
||||||
* - Checks if the data from the request is stored
|
* - 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<Session> checkSession(String sessionId) throws IllegalArgumentException {
|
public Optional<Session> checkSession(String sessionId) throws IllegalArgumentException {
|
||||||
Session session = sessions.get(sessionId);
|
Session session = sessions.getIfPresent(sessionId);
|
||||||
|
|
||||||
if(session == null) {
|
if(session == null) {
|
||||||
throw new IllegalArgumentException("Invalid session ID: " + sessionId);
|
throw new IllegalArgumentException("Invalid session ID: " + sessionId);
|
||||||
@@ -92,12 +111,15 @@ public class QuizService {
|
|||||||
*/
|
*/
|
||||||
@Scheduled(fixedDelay = 5000)
|
@Scheduled(fixedDelay = 5000)
|
||||||
protected void processQueue() {
|
protected void processQueue() {
|
||||||
|
System.out.print("Schedule!");
|
||||||
Request request = requests.poll();
|
Request request = requests.poll();
|
||||||
if (request != null) {
|
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 {
|
try {
|
||||||
List<Question> questions = extractQuestions(json);
|
List<Question> questions = extractQuestions(json);
|
||||||
Session session = sessions.get(request.getSessionId());
|
Session session = sessions.getIfPresent(request.getSessionId());
|
||||||
session.setQuestions(questions);
|
session.setQuestions(questions);
|
||||||
sessions.put(request.getSessionId(), session);
|
sessions.put(request.getSessionId(), session);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -112,6 +134,7 @@ public class QuizService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives: sessionId and answers
|
* Receives: sessionId and answers
|
||||||
|
*
|
||||||
* Actions:
|
* Actions:
|
||||||
* - retrieves session from cache
|
* - retrieves session from cache
|
||||||
* - Deletes session from cache
|
* - Deletes session from cache
|
||||||
@@ -119,8 +142,16 @@ public class QuizService {
|
|||||||
* - Calculates score
|
* - Calculates score
|
||||||
* Returns: calculated 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!";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
package com.ninjaws.quiz;
|
package com.ninjaws.quiz;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
class QuizApplicationTests {
|
class QuizApplicationTests {
|
||||||
|
|
||||||
@Test
|
// @Test
|
||||||
void contextLoads() {
|
// void contextLoads() {
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, Session> 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<Session> 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(){
|
||||||
|
|
||||||
|
// }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user