diff --git a/backend/src/main/java/com/ninjaws/quiz/components/StartupRunner.java b/backend/src/main/java/com/ninjaws/quiz/components/StartupRunner.java index 1885a81..129f975 100644 --- a/backend/src/main/java/com/ninjaws/quiz/components/StartupRunner.java +++ b/backend/src/main/java/com/ninjaws/quiz/components/StartupRunner.java @@ -1,27 +1,40 @@ package com.ninjaws.quiz.components; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; -import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; -/** - * Sets up the database, and adds the categories - * - * Fills up the primary database - * - * Then, at night, creates a new database, adds categories, and then starts filling it up - * When full, swaps the databases, and wipes the old one - */ -public class StartupRunner implements CommandLineRunner { - private final JdbcTemplate primaryJdbcTemplate; +import com.ninjaws.quiz.services.ApiService; +import com.ninjaws.quiz.services.StateManagementService; - public StartupRunner(JdbcTemplate primaryJdbcTemplate) { - this.primaryJdbcTemplate = primaryJdbcTemplate; - } + +@Component +public class StartupRunner implements CommandLineRunner { + + @Autowired + private StateManagementService stateManagementService; + @Autowired + private ApiService apiService; @Override public void run(String... args) { - // Initialization logic here - primaryJdbcTemplate.execute("CREATE TABLE IF NOT EXISTS test_table (id INT PRIMARY KEY, name VARCHAR(255))"); - primaryJdbcTemplate.update("INSERT INTO test_table (id, name) VALUES (?, ?)", 1, "Sample Data"); + + /** + * On startup: + * - Set ProcessingState to Startup + * - Store the categories + * - Get a sessiontoken + * + * * Inside the SchedulerService + * - Start filling the question table + * + * * Inside the Response Handling Service + * - No more questions? Stop + * - Set ProcessingState to Normal + */ + + stateManagementService.setCurrentProcessingState(StateManagementService.ProcessingState.STARTUP); + apiService.getCategories(); + apiService.getSessionToken(); } } diff --git a/backend/src/main/java/com/ninjaws/quiz/config/DataSourceConfig.java b/backend/src/main/java/com/ninjaws/quiz/config/DataSourceConfig.txt similarity index 100% rename from backend/src/main/java/com/ninjaws/quiz/config/DataSourceConfig.java rename to backend/src/main/java/com/ninjaws/quiz/config/DataSourceConfig.txt diff --git a/backend/src/main/java/com/ninjaws/quiz/controllers/QuizController.java b/backend/src/main/java/com/ninjaws/quiz/controllers/QuizController.java index b9f592e..fca6518 100644 --- a/backend/src/main/java/com/ninjaws/quiz/controllers/QuizController.java +++ b/backend/src/main/java/com/ninjaws/quiz/controllers/QuizController.java @@ -23,6 +23,7 @@ public class QuizController { @Autowired private QuizService quizService; + /** * Start a session * @param amount The amount of questsions diff --git a/backend/src/main/java/com/ninjaws/quiz/entities/BaseQuestionEntity.java b/backend/src/main/java/com/ninjaws/quiz/entities/BaseQuestionEntity.java new file mode 100644 index 0000000..bd4f7a7 --- /dev/null +++ b/backend/src/main/java/com/ninjaws/quiz/entities/BaseQuestionEntity.java @@ -0,0 +1,34 @@ +package com.ninjaws.quiz.entities; + +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import lombok.Setter; + +@MappedSuperclass +@Getter +@Setter +public class BaseQuestionEntity { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @ManyToOne + @JoinColumn(name = "category", nullable = false) + private Category category; + + private String difficulty; + private String type; + private String question; + + private String correctAnswer; + // @ElementCollection // This stores it in a separate table, it's just a set of strings so not required right now + private List incorrectAnswers; +} diff --git a/backend/src/main/java/com/ninjaws/quiz/entities/QuestionCollectingEntity.java b/backend/src/main/java/com/ninjaws/quiz/entities/QuestionCollectingEntity.java new file mode 100644 index 0000000..bc5370e --- /dev/null +++ b/backend/src/main/java/com/ninjaws/quiz/entities/QuestionCollectingEntity.java @@ -0,0 +1,10 @@ +package com.ninjaws.quiz.entities; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "question_collecting") +public class QuestionCollectingEntity extends BaseQuestionEntity{ + +} diff --git a/backend/src/main/java/com/ninjaws/quiz/entities/QuestionEntity.java b/backend/src/main/java/com/ninjaws/quiz/entities/QuestionEntity.java index 753254d..24aece9 100644 --- a/backend/src/main/java/com/ninjaws/quiz/entities/QuestionEntity.java +++ b/backend/src/main/java/com/ninjaws/quiz/entities/QuestionEntity.java @@ -1,36 +1,10 @@ package com.ninjaws.quiz.entities; -import java.util.List; - import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; -import lombok.Getter; -import lombok.Setter; @Entity @Table(name = "question") -@Getter -@Setter -public class QuestionEntity { +public class QuestionEntity extends BaseQuestionEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "category", nullable = false) - private Category category; - - private String difficulty; - private String type; - private String question; - - //* Hidden from the user */ - private String correctAnswer; - private List incorrectAnswers; } diff --git a/backend/src/main/java/com/ninjaws/quiz/models/QuizSettings.java b/backend/src/main/java/com/ninjaws/quiz/models/QuizSettings.java index cb82d5e..4693f8a 100644 --- a/backend/src/main/java/com/ninjaws/quiz/models/QuizSettings.java +++ b/backend/src/main/java/com/ninjaws/quiz/models/QuizSettings.java @@ -1,8 +1,5 @@ package com.ninjaws.quiz.models; -import com.ninjaws.quiz.models.QuizSettings.Category; -import com.ninjaws.quiz.models.QuizSettings.Difficulty; - import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; @@ -13,45 +10,9 @@ import lombok.Setter; @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 Integer category; private String difficulty; private String type; @@ -81,24 +42,6 @@ public class QuizSettings { * 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(); - // } - public String toApiQuery() { StringBuilder query = new StringBuilder("?amount=" + amount); diff --git a/backend/src/main/java/com/ninjaws/quiz/models/SessionTokenDTO.java b/backend/src/main/java/com/ninjaws/quiz/models/SessionTokenDTO.java new file mode 100644 index 0000000..193a48e --- /dev/null +++ b/backend/src/main/java/com/ninjaws/quiz/models/SessionTokenDTO.java @@ -0,0 +1,15 @@ +package com.ninjaws.quiz.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class SessionTokenDTO { + @JsonProperty("response_code") + private int responseCode; + @JsonProperty("token") + private String token; +} diff --git a/backend/src/main/java/com/ninjaws/quiz/repositories/QuestionCollectingRepository.java b/backend/src/main/java/com/ninjaws/quiz/repositories/QuestionCollectingRepository.java new file mode 100644 index 0000000..ae6214c --- /dev/null +++ b/backend/src/main/java/com/ninjaws/quiz/repositories/QuestionCollectingRepository.java @@ -0,0 +1,14 @@ +package com.ninjaws.quiz.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.ninjaws.quiz.entities.QuestionCollectingEntity; + +/** + * This table is used for collecting questions from the external API + * When it finishes, the data from this repository will replace the data in the QuestionRepository, to ensure it stays fresh + * This should be scheduled once every day + */ +public interface QuestionCollectingRepository extends JpaRepository{ + +} diff --git a/backend/src/main/java/com/ninjaws/quiz/repositories/QuestionRepository.java b/backend/src/main/java/com/ninjaws/quiz/repositories/QuestionRepository.java index 4ec9de2..afcc321 100644 --- a/backend/src/main/java/com/ninjaws/quiz/repositories/QuestionRepository.java +++ b/backend/src/main/java/com/ninjaws/quiz/repositories/QuestionRepository.java @@ -3,24 +3,37 @@ package com.ninjaws.quiz.repositories; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.ninjaws.quiz.entities.QuestionEntity; +import jakarta.transaction.Transactional; + public interface QuestionRepository extends JpaRepository{//, JpaSpecificationExecutor { + // @Query(value = "SELECT * FROM question AS q " + + // "WHERE (?1 IS NULL OR q.category = ?1) " + + // "AND (?2 IS NULL OR q.difficulty = ?2) " + + // "AND (?3 IS NULL OR q.type = ?3) " + + // "ORDER BY RAND() LIMIT ?4", nativeQuery = true) @Query(value = "SELECT * FROM question AS q " + - "WHERE (?1 IS NULL OR q.category = ?1) " + - "AND (?2 IS NULL OR q.difficulty = ?2) " + - "AND (?3 IS NULL OR q.type = ?3) " + - "ORDER BY RAND() LIMIT ?4", nativeQuery = true) - List lookupQuestions(Integer categoryId, String difficulty, String type, int limit); + "WHERE (:categoryId IS NULL OR q.category = :categoryId) " + + "AND (:difficulty IS NULL OR q.difficulty = :difficulty) " + + "AND (:type IS NULL OR q.type = :type) " + + "ORDER BY RAND() LIMIT :limit;", nativeQuery = true) + List lookupQuestions( + @Param("categoryId") Integer categoryId, + @Param("difficulty") String difficulty, + @Param("type") String type, + @Param("limit") int limit); - // public List getQuestionsBy(String type, String category, String difficulty, int limit) { - // Specification spec = Specification.where(QuestionSpecification.hasType(type)) - // .and(QuestionSpecification.hasCategory(category)) - // .and(QuestionSpecification.hasDifficulty(difficulty)); - - // return productRepository.findAll(spec); - // } + /** + * Moves all data from one table to another + */ + @Modifying + @Transactional + @Query(value="INSERT INTO question SELECT * FROM question_collecting;", nativeQuery=true) + void moveCollection(); } diff --git a/backend/src/main/java/com/ninjaws/quiz/services/ApiService.java b/backend/src/main/java/com/ninjaws/quiz/services/ApiService.java index 219e6fb..e4ce492 100644 --- a/backend/src/main/java/com/ninjaws/quiz/services/ApiService.java +++ b/backend/src/main/java/com/ninjaws/quiz/services/ApiService.java @@ -12,6 +12,7 @@ import com.ninjaws.quiz.models.ApiResponse; import com.ninjaws.quiz.models.CategoryResponse; import com.ninjaws.quiz.models.QuizSettings; import com.ninjaws.quiz.models.Request; +import com.ninjaws.quiz.models.SessionTokenDTO; import jakarta.validation.Valid; @@ -28,6 +29,7 @@ public class ApiService { private static final System.Logger logger = System.getLogger(ApiService.class.getName()); private final String apiUrl = "https://opentdb.com"; + private String sessionToken; public ApiService() { } @@ -55,21 +57,42 @@ public class ApiService { * Store them in the DataService */ public void collectBulk() { - // TODO: Make this happen only once, when a new database is generated - getCategories(); - String url = apiUrl + "/api.php?amount=50"; + String tokenStr = sessionToken == null ? "" : "&token="+sessionToken; + String url = apiUrl + "/api.php?amount=50" + tokenStr; try { ResponseEntity response = restTemplate.getForEntity(url, String.class); String json = response.getBody(); ApiResponse apiResponse = cleanerService.extractApiResponse(json); responseHandler.handleResponse(apiResponse); + /** + * A bit dirty, but otherwise it causes circular dependencies + * Makes sure the categories are refreshed after finishing to replace the questions, to make sure the ids match + * And resets the sessionToken, it will be remade during the next REFRESH + */ + if(apiResponse.getResponseCode() == 4) { + getCategories(); + sessionToken = null; + } + } catch (RestClientException | IOException e) { logger.log(System.Logger.Level.ERROR, "Critical failure", e); } } + public void getSessionToken() { + String url = apiUrl + "/api_token.php?command=request"; + try { + ResponseEntity response = restTemplate.getForEntity(url, String.class); + String json = response.getBody(); + SessionTokenDTO tokenDto = cleanerService.extractSessionToken(json); + sessionToken = tokenDto.getToken(); + + } catch (RestClientException | IOException e) { + logger.log(System.Logger.Level.ERROR, "Critical failure", e); + } + } public void getCategories() { String url = apiUrl + "/api_category.php"; diff --git a/backend/src/main/java/com/ninjaws/quiz/services/CleanerService.java b/backend/src/main/java/com/ninjaws/quiz/services/CleanerService.java index 9c08359..7b26710 100644 --- a/backend/src/main/java/com/ninjaws/quiz/services/CleanerService.java +++ b/backend/src/main/java/com/ninjaws/quiz/services/CleanerService.java @@ -10,12 +10,20 @@ import org.springframework.stereotype.Service; import com.fasterxml.jackson.databind.ObjectMapper; import com.ninjaws.quiz.entities.Category; +import com.ninjaws.quiz.entities.QuestionCollectingEntity; import com.ninjaws.quiz.entities.QuestionEntity; import com.ninjaws.quiz.models.ApiResponse; import com.ninjaws.quiz.models.CategoryResponse; import com.ninjaws.quiz.models.QuestionDTO; +import com.ninjaws.quiz.models.SessionTokenDTO; import com.ninjaws.quiz.repositories.CategoryRepository; +/** + * Deals with conversions from one form of data to another + * Json to Object + * Entity to DTO + * etc + */ @Service public class CleanerService { @@ -44,6 +52,10 @@ public class CleanerService { return dtos; } + public SessionTokenDTO extractSessionToken(String json) throws IOException { + return objectMapper.readValue(json, SessionTokenDTO.class); + } + public QuestionDTO questionEntityToDTO(QuestionEntity entity) { QuestionDTO dto = new QuestionDTO(); dto.setQuestion(entity.getQuestion()); @@ -63,7 +75,7 @@ public class CleanerService { } return entities; } - + public QuestionEntity questionDTOtoEntity(QuestionDTO dto) { QuestionEntity entity = new QuestionEntity(); Category category = categoryRepository.findByName(decodeString(dto.getCategory())).orElseThrow(() -> new IllegalStateException("Category not in DB!")); @@ -76,6 +88,30 @@ public class CleanerService { return entity; } + + // TODO: Find a simpler way to convert from one table to another if the data is identical so this nonsense isn't necessary + public List questionListDTOtoCollectingEntity(List dtos) { + List entities = new ArrayList<>(); + for (int i = 0; i < dtos.size(); i++) { + QuestionDTO dto = dtos.get(i); + entities.add(questionDTOtoCollectingEntity(dto)); + } + return entities; + } + + public QuestionCollectingEntity questionDTOtoCollectingEntity(QuestionDTO dto) { + QuestionCollectingEntity entity = new QuestionCollectingEntity(); + Category category = categoryRepository.findByName(decodeString(dto.getCategory())).orElseThrow(() -> new IllegalStateException("Category not in DB!")); + entity.setCategory(category); + entity.setDifficulty(dto.getDifficulty()); + entity.setType(dto.getType()); + entity.setQuestion(dto.getQuestion()); + entity.setCorrectAnswer(dto.getCorrectAnswer()); + entity.setIncorrectAnswers(dto.getIncorrectAnswers()); + return entity; + } + + /** * For some reason, the categories come with special characters, * But the categories from the questions dont diff --git a/backend/src/main/java/com/ninjaws/quiz/services/DataService.java b/backend/src/main/java/com/ninjaws/quiz/services/DataService.java index adb2469..de2f114 100644 --- a/backend/src/main/java/com/ninjaws/quiz/services/DataService.java +++ b/backend/src/main/java/com/ninjaws/quiz/services/DataService.java @@ -6,17 +6,22 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.ninjaws.quiz.entities.Category; +import com.ninjaws.quiz.entities.QuestionCollectingEntity; import com.ninjaws.quiz.entities.QuestionEntity; import com.ninjaws.quiz.models.QuizSettings; import com.ninjaws.quiz.repositories.CategoryRepository; +import com.ninjaws.quiz.repositories.QuestionCollectingRepository; import com.ninjaws.quiz.repositories.QuestionRepository; +import jakarta.transaction.Transactional; + @Service public class DataService { @Autowired private QuestionRepository questionRepository; - + @Autowired + private QuestionCollectingRepository collectingRepository; @Autowired private CategoryRepository categoryRepository; @@ -32,34 +37,31 @@ public class DataService { return questionRepository.saveAll(questions); } - // public List getRandomQuestions(String type, String category, String difficulty, int limit) { - // return questionRepository.findRandomQuestions(type, limit); - // } - - public List lookupQuestions(QuizSettings quizSettings) { - // Long category = Long.valueOf(quizSettings.getCategory().getApiValue() == 1 ? null : quizSettings.getCategory().getApiValue()); - // String difficulty = quizSettings.getDifficulty() == QuizSettings.Difficulty.ANY ? null : quizSettings.getDifficulty().getApiValue(); - // String type = quizSettings.getType() == QuizSettings.Type.ANY ? null : quizSettings.getType().getApiValue(); - // System.out.println(category); - // System.out.println(difficulty); - // System.out.println(type); - - return questionRepository.lookupQuestions(quizSettings.getCategory(), quizSettings.getDifficulty(), quizSettings.getType(), quizSettings.getAmount()); - } - /** * Tries to get questions that match the specified QuizSettings + * * @return The questions if the query was possible, and an empty array if not */ - // public List getByQuizSettings(QuizSettings quizSettings) { - // Specification spec = Specification.where(QuestionSpecification.hasType(type)) - // .and(QuestionSpecification.hasCategory(category)) - // .and(QuestionSpecification.hasDifficulty(difficulty)); - - // return productRepository.findAll(spec); - // } + public List lookupQuestions(QuizSettings quizSettings) { + return questionRepository.lookupQuestions(quizSettings.getCategory(), quizSettings.getDifficulty(), quizSettings.getType(), quizSettings.getAmount()); + } public List saveCategories(List categories) { return categoryRepository.saveAll(categories); } + + /** + * Moving the collected questions to the questions list + * + */ + @Transactional + public void migrateQuestions() { + questionRepository.deleteAll(); + questionRepository.moveCollection(); + collectingRepository.deleteAll(); + } + + public List collectQuestions(List questions) { + return collectingRepository.saveAll(questions); + } } diff --git a/backend/src/main/java/com/ninjaws/quiz/services/DatabaseScheduler.java b/backend/src/main/java/com/ninjaws/quiz/services/DatabaseScheduler.java deleted file mode 100644 index 83686c1..0000000 --- a/backend/src/main/java/com/ninjaws/quiz/services/DatabaseScheduler.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.ninjaws.quiz.services; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -@Service -public class DatabaseScheduler { - @Autowired - private JdbcTemplate secondaryJdbcTemplate; - - @Scheduled(fixedRate = 60000) // Runs every 60 seconds - public void createAndFillSecondaryDatabase() { - // Create tables and insert data into the secondary database - secondaryJdbcTemplate. - .execute("CREATE TABLE IF NOT EXISTS sample_table (id INT PRIMARY KEY, name VARCHAR(255))"); - secondaryJdbcTemplate.update("INSERT INTO sample_table (id, name) VALUES (?, ?)", 1, "Sample Data"); - } -} diff --git a/backend/src/main/java/com/ninjaws/quiz/services/QuizService.java b/backend/src/main/java/com/ninjaws/quiz/services/QuizService.java index d511850..5bb92b3 100644 --- a/backend/src/main/java/com/ninjaws/quiz/services/QuizService.java +++ b/backend/src/main/java/com/ninjaws/quiz/services/QuizService.java @@ -38,7 +38,7 @@ public class QuizService { /** First, we check if there are already enough items in our db */ List questionsRetrieved = dataService.lookupQuestions(quizSettings); - if(!questionsRetrieved.isEmpty()) { + if(!questionsRetrieved.isEmpty() && questionsRetrieved.size() == quizSettings.getAmount()) { session.setQuestions(cleanerService.questionListEntitytoDTO(questionsRetrieved)); } @@ -47,7 +47,7 @@ public class QuizService { /** * If the questions were retrieved from the DB, then it doesn't need to be added to the queue */ - if(!questionsRetrieved.isEmpty()) { + if(!questionsRetrieved.isEmpty() && questionsRetrieved.size() == quizSettings.getAmount()) { return Optional.of(session.getId()); } @@ -81,9 +81,10 @@ public class QuizService { } /** Anything other than statuscode 0 doesn't result in active sessions, so remove them from the cache after reporting to the frontend */ - if(session.getStatusCode() != 0) { - cacheService.removeFromCache(session.getId()); - } + // if(session.getStatusCode() != 0) { + /** Regardless of status code, this link is not reusable */ + cacheService.removeFromCache(session.getId()); + // } return Optional.of(session); } diff --git a/backend/src/main/java/com/ninjaws/quiz/services/ResponseHandlingService.java b/backend/src/main/java/com/ninjaws/quiz/services/ResponseHandlingService.java index 96b2dd0..4d07c9f 100644 --- a/backend/src/main/java/com/ninjaws/quiz/services/ResponseHandlingService.java +++ b/backend/src/main/java/com/ninjaws/quiz/services/ResponseHandlingService.java @@ -10,6 +10,8 @@ import com.ninjaws.quiz.models.Session; @Service public class ResponseHandlingService { + @Autowired + private StateManagementService stateManager; @Autowired private CacheService cacheService; @Autowired @@ -33,7 +35,8 @@ public class ResponseHandlingService { case 0: // Success updateSessionInCache(session, response); - dataService.saveQuestions(cleanerService.questionListDTOtoEntity(response.getResults())); + /** Managing the token gets difficult since there is a timelimit, so simply dont add these, let the bulk collector do it */ + // dataService.saveQuestions(cleanerService.questionListDTOtoEntity(response.getResults())); break; case 1: // Not enough questions to satisfy the request. @@ -67,11 +70,26 @@ public class ResponseHandlingService { public void handleResponse(ApiResponse response) { switch (response.getResponseCode()) { case 0: - dataService.saveQuestions(cleanerService.questionListDTOtoEntity(response.getResults())); + if(stateManager.getCurrentProcessingState() == StateManagementService.ProcessingState.STARTUP) { + /** Initial saving during the startup */ + dataService.saveQuestions(cleanerService.questionListDTOtoEntity(response.getResults())); + } else if (stateManager.getCurrentProcessingState() == StateManagementService.ProcessingState.REFRESH) { + /** Background collecting during the night */ + dataService.collectQuestions(cleanerService.questionListDTOtoCollectingEntity(response.getResults())); + } break; case 4: - // Signal DataService that we have extracted all the data. Stop gathering, switch to caching-mode - // TODO: Signal DB that it's full, start db-swapping process + // Signal DataService that we have extracted all the data. Stop gathering, + // TODO: Switch to caching-mode + + /** + * If it was in the REFRESH state, move the data from collecting to the used questions + * Otherwise simply switch to the NORMAL state + */ + if (stateManager.getCurrentProcessingState() == StateManagementService.ProcessingState.REFRESH) { + dataService.migrateQuestions(); + } + stateManager.setCurrentProcessingState(StateManagementService.ProcessingState.NORMAL); break; case 5: // Should be prevented by the scheduler diff --git a/backend/src/main/java/com/ninjaws/quiz/services/SchedulerService.java b/backend/src/main/java/com/ninjaws/quiz/services/SchedulerService.java index 41b7dc4..a8f474a 100644 --- a/backend/src/main/java/com/ninjaws/quiz/services/SchedulerService.java +++ b/backend/src/main/java/com/ninjaws/quiz/services/SchedulerService.java @@ -13,6 +13,8 @@ public class SchedulerService { private QueueService queueService; @Autowired private ApiService apiService; + @Autowired + private StateManagementService stateService; public SchedulerService() {} @@ -25,11 +27,23 @@ public class SchedulerService { @Scheduled(fixedDelay = 5000) protected void processQueue() { Request request = queueService.popFromQueue(); + if (request != null) { apiService.handleRequest(request); } else { - apiService.collectBulk(); + if (stateService.getCurrentProcessingState() == StateManagementService.ProcessingState.STARTUP || stateService.getCurrentProcessingState() == StateManagementService.ProcessingState.REFRESH) { + apiService.collectBulk(); + } } } + + /** + * Starts the renewing of the questions, to ensure they stay fresh + */ + @Scheduled(cron = "0 0 1 * * ?") + protected void StartDbRefresh() { + apiService.getSessionToken(); + stateService.setCurrentProcessingState(StateManagementService.ProcessingState.REFRESH); + } } diff --git a/backend/src/main/java/com/ninjaws/quiz/services/StateManagementService.java b/backend/src/main/java/com/ninjaws/quiz/services/StateManagementService.java new file mode 100644 index 0000000..b1b8c49 --- /dev/null +++ b/backend/src/main/java/com/ninjaws/quiz/services/StateManagementService.java @@ -0,0 +1,29 @@ +package com.ninjaws.quiz.services; + +import org.springframework.stereotype.Service; + +@Service +public class StateManagementService { + + /** + * STARTUP: When starting the app + * REFRESH: Scheduled at night, so the database stays fresh + * NORMAL: No special activities + */ + public enum ProcessingState { + STARTUP, + REFRESH, + NORMAL + } + + private ProcessingState currentProcessingState; + + public ProcessingState getCurrentProcessingState() { + return currentProcessingState; + } + + public void setCurrentProcessingState(ProcessingState currentProcessingState) { + this.currentProcessingState = currentProcessingState; + } + +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index f7d952d..451ac85 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -6,29 +6,29 @@ spring.h2.console.enabled=true # spring.h2.console.path=/console -# spring.datasource.url=jdbc:h2:mem:testdb -# spring.datasource.driverClassName=org.h2.Driver -# spring.datasource.username=sa -# spring.datasource.password= +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= -# spring.jpa.hibernate.ddl-auto=create-drop -# spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -# Primary database configuration -spring.datasource.primary.url=jdbc:h2:mem:testdb -# ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE -spring.datasource.primary.driver-class-name=org.h2.Driver -spring.datasource.primary.username=sa -spring.datasource.primary.password= -spring.jpa.primary.hibernate.ddl-auto=create-drop -spring.jpa.primary.database-platform=org.hibernate.dialect.H2Dialect +# # Primary database configuration +# spring.datasource.primary.url=jdbc:h2:mem:testdb +# # ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +# spring.datasource.primary.driver-class-name=org.h2.Driver +# spring.datasource.primary.username=sa +# spring.datasource.primary.password= +# spring.jpa.primary.hibernate.ddl-auto=create-drop +# spring.jpa.primary.database-platform=org.hibernate.dialect.H2Dialect -# Secondary database configuration -spring.datasource.secondary.url=jdbc:h2:mem:secondarydb -# ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE -spring.datasource.secondary.driver-class-name=org.h2.Driver -spring.datasource.secondary.username=sa -spring.datasource.secondary.password= -spring.jpa.secondary.hibernate.ddl-auto=create-drop -spring.jpa.secondary.database-platform=org.hibernate.dialect.H2Dialect \ No newline at end of file +# # Secondary database configuration +# spring.datasource.secondary.url=jdbc:h2:mem:secondarydb +# # ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +# spring.datasource.secondary.driver-class-name=org.h2.Driver +# spring.datasource.secondary.username=sa +# spring.datasource.secondary.password= +# spring.jpa.secondary.hibernate.ddl-auto=create-drop +# spring.jpa.secondary.database-platform=org.hibernate.dialect.H2Dialect \ No newline at end of file diff --git a/backend/src/test/java/com/ninjaws/quiz/services/CleanerServiceTest.txt b/backend/src/test/java/com/ninjaws/quiz/services/CleanerServiceTest.txt deleted file mode 100644 index bdb7919..0000000 --- a/backend/src/test/java/com/ninjaws/quiz/services/CleanerServiceTest.txt +++ /dev/null @@ -1,107 +0,0 @@ -package com.ninjaws.quiz.services; - -import java.io.IOException; -import java.util.Arrays; - -import static org.assertj.core.api.Assertions.assertThat; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import org.mockito.MockitoAnnotations; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.ninjaws.quiz.entities.Category; -import com.ninjaws.quiz.models.ApiResponse; -import com.ninjaws.quiz.models.QuestionDTO; - -// public class CleanerServiceTest { - -// @InjectMocks -// private CleanerService cleanerService; - -// @Mock -// private ObjectMapper objectMapper; - -// @BeforeEach -// public void setUp() { -// MockitoAnnotations.openMocks(this); -// } - -// @Test -// public void testExtractApiResponse() throws IOException { -// String json = "{\"response_code\":200,\"results\":[{\"type\":\"multiple choice\",\"difficulty\":\"easy\",\"category\":\"general knowledge\",\"question\":\"What is the capital of France?\",\"correct_answer\":\"Paris\",\"incorrect_answers\":[\"Berlin\",\"Madrid\",\"Rome\"]}]}"; // Example JSON input - -// Question question = new Question(); -// question.setType("multiple choice"); -// question.setDifficulty("easy"); -// question.setCategory("general knowledge"); -// question.setQuestion("What is the capital of France?"); -// question.setCorrectAnswer("Paris"); -// question.setIncorrectAnswers(Arrays.asList("Berlin", "Madrid", "Rome")); - -// ApiResponse expectedResponse = new ApiResponse(); -// expectedResponse.setResponseCode(200); -// expectedResponse.setResults(Arrays.asList(question)); -// when(objectMapper.readValue(json, ApiResponse.class)).thenReturn(expectedResponse); - -// ApiResponse actualResponse = cleanerService.extractApiResponse(json); -// assertThat(actualResponse).isNotNull(); -// assertThat(actualResponse.getResponseCode()).isEqualTo(expectedResponse.getResponseCode()); -// assertThat(actualResponse.getResults()).isNotEmpty(); -// assertThat(actualResponse.getResults().get(0).getQuestion()).isEqualTo(question.getQuestion()); -// verify(objectMapper, times(1)).readValue(json, ApiResponse.class); -// } -// } - -@ExtendWith(MockitoExtension.class) -public class CleanerServiceTest { - - @InjectMocks - private CleanerService cleanerService; - - @Mock - private ObjectMapper objectMapper; - - @BeforeEach - public void setUp() { - MockitoAnnotations.openMocks(this); - } - - @Test - public void testExtractApiResponse() throws IOException { - String json = "{\"response_code\":200,\"results\":[{\"type\":\"multiple choice\",\"difficulty\":\"easy\",\"category\":\"general knowledge\",\"question\":\"What is the capital of France?\",\"correct_answer\":\"Paris\",\"incorrect_answers\":[\"Berlin\",\"Madrid\",\"Rome\"]}]}"; // Example JSON input - - // Create a Category object for the question - Category category = new Category(); - category.setId(1L); - category.setName("General Knowledge"); - - // Create the Question object with the category - QuestionDTO question = new QuestionDTO(); - question.setType("multiple choice"); - question.setDifficulty("easy"); - question.setCategory(category); // Set the Category object here - question.setQuestion("What is the capital of France?"); - question.setCorrectAnswer("Paris"); - question.setIncorrectAnswers(Arrays.asList("Berlin", "Madrid", "Rome")); - - ApiResponse expectedResponse = new ApiResponse(); - expectedResponse.setResponseCode(200); - expectedResponse.setResults(Arrays.asList(question)); - when(objectMapper.readValue(json, ApiResponse.class)).thenReturn(expectedResponse); - - ApiResponse actualResponse = cleanerService.extractApiResponse(json); - assertThat(actualResponse).isNotNull(); - assertThat(actualResponse.getResponseCode()).isEqualTo(expectedResponse.getResponseCode()); - assertThat(actualResponse.getResults()).isNotEmpty(); - assertThat(actualResponse.getResults().get(0).getQuestion()).isEqualTo(question.getQuestion()); - assertThat(actualResponse.getResults().get(0).getCategory().getName()).isEqualTo(category.getName()); // Check category name - verify(objectMapper, times(1)).readValue(json, ApiResponse.class); - } -} \ No newline at end of file diff --git a/backend/src/test/java/com/ninjaws/quiz/services/DataServiceTest.txt b/backend/src/test/java/com/ninjaws/quiz/services/DataServiceTest.txt deleted file mode 100644 index faa94e2..0000000 --- a/backend/src/test/java/com/ninjaws/quiz/services/DataServiceTest.txt +++ /dev/null @@ -1,202 +0,0 @@ -package com.ninjaws.quiz.services; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import org.mockito.MockitoAnnotations; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.ninjaws.quiz.entities.Category; -import com.ninjaws.quiz.models.QuestionDTO; -import com.ninjaws.quiz.repositories.QuestionRepository; - -// public class DataServiceTest { - -// @InjectMocks -// private DataService dataService; - -// @Mock -// private QuestionRepository questionRepository; - -// private Question question; - - -// @BeforeEach -// public void setUp() { -// MockitoAnnotations.openMocks(this); - -// question = new Question(); -// question.setType("multiple choice"); -// question.setDifficulty("easy"); -// question.setCategory("general knowledge"); -// question.setQuestion("What is the capital of France?"); -// question.setCorrectAnswer("Paris"); -// question.setIncorrectAnswers(Arrays.asList("Berlin", "Madrid", "Rome")); -// } - -// @Test -// public void testSaveQuestion() { -// when(questionRepository.save(any(Question.class))).thenReturn(question); - -// Question savedQuestion = dataService.saveQuestion(question); -// assertThat(savedQuestion).isNotNull(); -// assertThat(savedQuestion.getQuestion()).isEqualTo("What is the capital of France?"); -// verify(questionRepository, times(1)).save(question); -// } - -// @Test -// public void testGetAllQuestions() { -// when(questionRepository.findAll()).thenReturn(Arrays.asList(question)); - -// List questions = dataService.getAllQuestions(); -// assertThat(questions).isNotEmpty(); -// assertThat(questions.size()).isEqualTo(1); -// assertThat(questions.get(0).getQuestion()).isEqualTo("What is the capital of France?"); -// verify(questionRepository, times(1)).findAll(); -// } - -// @Test -// public void testFindQuestionById() { -// when(questionRepository.findById(any(Long.class))).thenReturn(Optional.of(question)); - -// Optional foundQuestion = questionRepository.findById(1L); -// assertThat(foundQuestion).isPresent(); -// assertThat(foundQuestion.get().getQuestion()).isEqualTo("What is the capital of France?"); -// verify(questionRepository, times(1)).findById(1L); -// } - -// @Test -// public void testSaveQuestions() { -// Question question1 = new Question(); -// question1.setType("multiple choice"); -// question1.setDifficulty("easy"); -// question1.setCategory("general knowledge"); -// question1.setQuestion("What is the capital of France?"); -// question1.setCorrectAnswer("Paris"); -// question1.setIncorrectAnswers(Arrays.asList("Berlin", "Madrid", "Rome")); - -// Question question2 = new Question(); -// question2.setType("multiple choice"); -// question2.setDifficulty("medium"); -// question2.setCategory("science"); -// question2.setQuestion("What is the chemical symbol for water?"); -// question2.setCorrectAnswer("H2O"); -// question2.setIncorrectAnswers(Arrays.asList("CO2", "O2", "NaCl")); - -// when(questionRepository.saveAll(anyList())).thenReturn(Arrays.asList(question1, question2)); - -// List questions = dataService.saveQuestions(Arrays.asList(question1, question2)); - -// assertThat(questions).hasSize(2); -// assertThat(questions.get(0).getQuestion()).isEqualTo("What is the capital of France?"); -// assertThat(questions.get(1).getQuestion()).isEqualTo("What is the chemical symbol for water?"); -// verify(questionRepository, times(1)).saveAll(anyList()); -// } -// } - -@ExtendWith(MockitoExtension.class) -public class DataServiceTest { - - @InjectMocks - private DataService dataService; - - @Mock - private QuestionRepository questionRepository; - - private QuestionDTO question; - private Category category; - - @BeforeEach - public void setUp() { - MockitoAnnotations.openMocks(this); - - category = new Category(); - category.setId(1L); - category.setName("General Knowledge"); - - question = new QuestionDTO(); - question.setType("multiple choice"); - question.setDifficulty("easy"); - question.setCategory(category); // Set the Category object here - question.setQuestion("What is the capital of France?"); - question.setCorrectAnswer("Paris"); - question.setIncorrectAnswers(Arrays.asList("Berlin", "Madrid", "Rome")); - } - - @Test - public void testSaveQuestion() { - when(questionRepository.save(any(QuestionDTO.class))).thenReturn(question); - - QuestionDTO savedQuestion = dataService.saveQuestion(question); - assertThat(savedQuestion).isNotNull(); - assertThat(savedQuestion.getQuestion()).isEqualTo("What is the capital of France?"); - assertThat(savedQuestion.getCategory().getName()).isEqualTo("General Knowledge"); // Check category name - verify(questionRepository, times(1)).save(question); - } - - @Test - public void testGetAllQuestions() { - when(questionRepository.findAll()).thenReturn(Arrays.asList(question)); - - List questions = dataService.getAllQuestions(); - assertThat(questions).isNotEmpty(); - assertThat(questions.size()).isEqualTo(1); - assertThat(questions.get(0).getQuestion()).isEqualTo("What is the capital of France?"); - assertThat(questions.get(0).getCategory().getName()).isEqualTo("General Knowledge"); // Check category name - verify(questionRepository, times(1)).findAll(); - } - - @Test - public void testFindQuestionById() { - when(questionRepository.findById(any(Long.class))).thenReturn(Optional.of(question)); - - Optional foundQuestion = questionRepository.findById(1L); - assertThat(foundQuestion).isPresent(); - assertThat(foundQuestion.get().getQuestion()).isEqualTo("What is the capital of France?"); - assertThat(foundQuestion.get().getCategory().getName()).isEqualTo("General Knowledge"); // Check category name - verify(questionRepository, times(1)).findById(1L); - } - - @Test - public void testSaveQuestions() { - QuestionDTO question1 = new QuestionDTO(); - question1.setType("multiple choice"); - question1.setDifficulty("easy"); - question1.setCategory(category); // Set the Category object here - question1.setQuestion("What is the capital of France?"); - question1.setCorrectAnswer("Paris"); - question1.setIncorrectAnswers(Arrays.asList("Berlin", "Madrid", "Rome")); - - QuestionDTO question2 = new QuestionDTO(); - Category category2 = new Category(); - category2.setId(2L); - category2.setName("Science"); - question2.setType("multiple choice"); - question2.setDifficulty("medium"); - question2.setCategory(category2); // Set the Category object here - question2.setQuestion("What is the chemical symbol for water?"); - question2.setCorrectAnswer("H2O"); - question2.setIncorrectAnswers(Arrays.asList("CO2", "O2", "NaCl")); - - when(questionRepository.saveAll(anyList())).thenReturn(Arrays.asList(question1, question2)); - - List questions = dataService.saveQuestions(Arrays.asList(question1, question2)); - - assertThat(questions).hasSize(2); - assertThat(questions.get(0).getQuestion()).isEqualTo("What is the capital of France?"); - assertThat(questions.get(1).getQuestion()).isEqualTo("What is the chemical symbol for water?"); - verify(questionRepository, times(1)).saveAll(anyList()); - } -} diff --git a/backend/src/test/java/com/ninjaws/quiz/services/backupApiServiceTest.txt b/backend/src/test/java/com/ninjaws/quiz/services/backupApiServiceTest.txt deleted file mode 100644 index 1b3f44e..0000000 --- a/backend/src/test/java/com/ninjaws/quiz/services/backupApiServiceTest.txt +++ /dev/null @@ -1,114 +0,0 @@ -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.handleRequest(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/backupQuizServiceTest.txt b/backend/src/test/java/com/ninjaws/quiz/services/backupQuizServiceTest.txt deleted file mode 100644 index 130fbd0..0000000 --- a/backend/src/test/java/com/ninjaws/quiz/services/backupQuizServiceTest.txt +++ /dev/null @@ -1,72 +0,0 @@ -package com.ninjaws.quiz.services; - -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(){ - - // } -} diff --git a/scripts/LoadTestMultiple.sh b/scripts/LoadTestMultiple.sh index 352f191..1bc9f85 100755 --- a/scripts/LoadTestMultiple.sh +++ b/scripts/LoadTestMultiple.sh @@ -2,9 +2,9 @@ start_time=$(date +%s) -for i in {1..6}; do - ./LoadTestOne-WithParams.sh & - sleep 0.1 +for i in {1..1000}; do + ./LoadTestOne-Silent.sh & +# sleep 0.1 done wait # Wait for all background jobs to finish diff --git a/scripts/LoadTestOne-Silent.sh b/scripts/LoadTestOne-Silent.sh new file mode 100755 index 0000000..a76f8b3 --- /dev/null +++ b/scripts/LoadTestOne-Silent.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Step 1: Get the sessionId from the questions endpoint +sessionIdJson=$(curl -s "http://localhost:9090/quiz/questions?amount=10&category=23&difficulty=medium&type=multiple") + +# Extract the status code +statusCode=$(echo "$sessionIdJson" | grep -o '"statusCode":[0-9]*' | sed 's/"statusCode"://') + +if [ "$statusCode" -ne 0 ]; then + exit 1 +fi + +# Extract the session ID +sessionId=$(echo "$sessionIdJson" | grep -o '"sessionId":"[^"]*"' | sed 's/"sessionId":"//;s/"//') + +if [ -z "$sessionId" ]; then + exit 1 +fi + +# Step 2: Poll the status endpoint every 2 seconds +while true; do + status_response=$(curl -s "http://localhost:9090/quiz/status/$sessionId") + + if [ "$status_response" != "null" ]; then + # Extract the status code from the response + statusCode=$(echo "$status_response" | grep -o '"statusCode":[0-9]*' | sed 's/"statusCode"://') + + if [ "$statusCode" -ne 0 ]; then + exit 1 + fi + + # Check if the response is JSON (starts with '{') + if [[ $status_response == \{* ]]; then + break + fi + fi + + sleep 2 +done diff --git a/scripts/LoadTestOne-WithParams.sh b/scripts/LoadTestOne-WithParams.sh index f23ce00..42d65f4 100755 --- a/scripts/LoadTestOne-WithParams.sh +++ b/scripts/LoadTestOne-WithParams.sh @@ -1,7 +1,6 @@ #!/bin/bash - # Step 1: Get the sessionId from the questions endpoint -sessionIdJson=$(curl -s "http://localhost:9090/quiz/questions?amount=25&category=27&difficulty=medium&type=boolean") +sessionIdJson=$(curl -s "http://localhost:9090/quiz/questions?amount=20&category=27&difficulty=medium&type=multiple") statusCode=$(echo "$sessionIdJson" | grep -o '"statusCode":[0-9]*' | sed 's/"statusCode"://') @@ -22,8 +21,7 @@ echo "Received sessionId: $sessionId" # Step 2: Poll the status endpoint every 2 seconds while true; do status_response=$(curl -s http://localhost:9090/quiz/status/"$sessionId") - - if [ $? -eq 0 ]; then + if [ "$status_response" != "null" ] && [ $? -eq 0 ]; then statusCode=$(echo "$status_response" | grep -o '"statusCode":[0-9]*' | sed 's/"statusCode"://') @@ -34,7 +32,6 @@ while true; do # Check if the response is JSON (starts with '{') if [[ $status_response == \{* ]]; then -# echo "$statusCode" echo "$sessionId: done! status: $statusCode" break fi diff --git a/scripts/LoadTestOne.sh b/scripts/LoadTestOne.sh index feb32eb..a85eab8 100755 --- a/scripts/LoadTestOne.sh +++ b/scripts/LoadTestOne.sh @@ -1,5 +1,4 @@ #!/bin/bash - # Step 1: Get the sessionId from the questions endpoint sessionIdJson=$(curl -s http://localhost:9090/quiz/questions?amount=5) @@ -22,16 +21,22 @@ echo "Received sessionId: $sessionId" # Step 2: Poll the status endpoint every 2 seconds while true; do status_response=$(curl -s http://localhost:9090/quiz/status/"$sessionId") + if [ "$status_response" != "null" ] && [ $? -eq 0 ]; then + + statusCode=$(echo "$status_response" | grep -o '"statusCode":[0-9]*' | sed 's/"statusCode"://') + + if [ "$statusCode" -ne 0 ]; then + echo "Exiting: statusCode is $statusCode" + exit 1 + fi - if [ $? -eq 0 ]; then # Check if the response is JSON (starts with '{') if [[ $status_response == \{* ]]; then - echo "$sessionId: done!" + echo "$sessionId: done! status: $statusCode" break fi else - echo "Status check failed, exiting..." - exit 1 + echo "Status check failed, retrying..." fi sleep 2 diff --git a/scripts/benchmarks/DB and Queue - All Params - 1000 and 10000 sessions.png b/scripts/benchmarks/DB and Queue - All Params - 1000 and 10000 sessions.png new file mode 100644 index 0000000..e9032bb Binary files /dev/null and b/scripts/benchmarks/DB and Queue - All Params - 1000 and 10000 sessions.png differ