28 changed files with 365 additions and 746 deletions
@@ -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();
}
}
@@ -1,46 +0,0 @@
package com.ninjaws.quiz.config;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
public class DataSourceConfig {
@Primary
@Bean(name = "primaryDataSourceProperties")
@ConfigurationProperties("spring.datasource.primary")
public DataSourceProperties primaryDataSourceProperties() {
return new DataSourceProperties();
}
@Primary
@Bean(name = "primaryDataSource")
public DataSource primaryDataSource() {
return primaryDataSourceProperties().initializeDataSourceBuilder().build();
}
@Bean(name = "secondaryDataSourceProperties")
@ConfigurationProperties("spring.datasource.secondary")
public DataSourceProperties secondaryDataSourceProperties() {
return new DataSourceProperties();
}
@Bean(name = "secondaryDataSource")
public DataSource secondaryDataSource() {
return secondaryDataSourceProperties().initializeDataSourceBuilder().build();
}
@Bean
public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource ds) {
return new JdbcTemplate(ds);
}
@Bean
public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource ds) {
return new JdbcTemplate(ds);
}
}
@@ -23,6 +23,7 @@ public class QuizController {
@Autowired
private QuizService quizService;
/**
* Start a session
* @param amount The amount of questsions
@@ -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<String> incorrectAnswers;
}
@@ -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{
}
@@ -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<String> incorrectAnswers;
}
@@ -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);
@@ -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;
}
@@ -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<QuestionCollectingEntity, Long>{
}
@@ -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<QuestionEntity, Long>{//, JpaSpecificationExecutor<Question> {
// @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<QuestionEntity> 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<QuestionEntity> lookupQuestions(
@Param("categoryId") Integer categoryId,
@Param("difficulty") String difficulty,
@Param("type") String type,
@Param("limit") int limit);
// public List<Question> getQuestionsBy(String type, String category, String difficulty, int limit) {
// Specification<Product> 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();
}
@@ -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<String> 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<String> 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";
@@ -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());
@@ -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<QuestionCollectingEntity> questionListDTOtoCollectingEntity(List<QuestionDTO> dtos) {
List<QuestionCollectingEntity> 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
@@ -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<Question> getRandomQuestions(String type, String category, String difficulty, int limit) {
// return questionRepository.findRandomQuestions(type, limit);
// }
public List<QuestionEntity> 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<Question> getByQuizSettings(QuizSettings quizSettings) {
// Specification<Product> spec = Specification.where(QuestionSpecification.hasType(type))
// .and(QuestionSpecification.hasCategory(category))
// .and(QuestionSpecification.hasDifficulty(difficulty));
// return productRepository.findAll(spec);
// }
public List<QuestionEntity> lookupQuestions(QuizSettings quizSettings) {
return questionRepository.lookupQuestions(quizSettings.getCategory(), quizSettings.getDifficulty(), quizSettings.getType(), quizSettings.getAmount());
}
public List<Category> saveCategories(List<Category> 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<QuestionCollectingEntity> collectQuestions(List<QuestionCollectingEntity> questions) {
return collectingRepository.saveAll(questions);
}
}
@@ -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");
}
}
@@ -38,7 +38,7 @@ public class QuizService {
/** First, we check if there are already enough items in our db */
List<QuestionEntity> 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);
}
@@ -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
@@ -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);
}
}
@@ -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;
}
}
@@ -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
# # 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
@@ -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);
}
}
@@ -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<Question> 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<Question> 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<Question> 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<QuestionDTO> 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<QuestionDTO> 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<QuestionDTO> 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());
}
}
@@ -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);
}
}
@@ -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<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 &quot;Altis&quot; 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&#039;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 &amp; Manga\",\"question\":\"In &quot;Highschool DxD&quot;, 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(){
// }
}
+3 -3
View File
@@ -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
+38
View File
@@ -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
+2 -5
View File
@@ -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
+10 -5
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB