diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 670d9399..64a9e99c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,135 +1,151 @@ -name: Deploy on release - -on: - release: - types: [published] -jobs: - unit-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - uses: actions/checkout@v4 - - run: npm --prefix webapp ci - - run: npm --prefix webapp test -- --coverage - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - run: mvn clean verify - working-directory: api - env: - DATABASE_USER: ${{ secrets.DATABASE_USER }} - DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - - name: Analyze with SonarCloud - uses: sonarsource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - e2e-tests: - needs: [ unit-tests ] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - run: npm --prefix webapp install - - run: npm --prefix webapp run build - # - run: npm --prefix webapp run test:e2e - docker-push-api: - runs-on: ubuntu-latest - needs: [ e2e-tests ] - steps: - - uses: actions/checkout@v4 - - name: Publish to Registry - uses: elgohr/Publish-Docker-Github-Action@v5 - env: - DATABASE_USER: ${{ secrets.DATABASE_USER }} - DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - with: - name: arquisoft/wiq_en2b/api - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - workdir: api - buildargs: | - DATABASE_USER - DATABASE_PASSWORD - JWT_SECRET - docker-push-webapp: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [ e2e-tests ] - steps: - - - uses: actions/checkout@v4 - - - name: Create .env file - run: echo "REACT_APP_API_ENDPOINT=http://${{ secrets.DEPLOY_HOST }}:8080" > webapp/.env - - - name: Publish to Registry - uses: elgohr/Publish-Docker-Github-Action@v5 - env: - REACT_APP_API_ENDPOINT: http://${{ secrets.DEPLOY_HOST }}:8080 - teamname: wiq_en2b - with: - name: arquisoft/wiq_en2b/webapp - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - workdir: webapp - buildargs: | - REACT_APP_API_ENDPOINT - docker-push-question-generator: - runs-on: ubuntu-latest - needs: [ e2e-tests ] - steps: - - uses: actions/checkout@v4 - - name: Publish to Registry - uses: elgohr/Publish-Docker-Github-Action@v5 - env: - DATABASE_USER: ${{ secrets.DATABASE_USER }} - DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} - with: - name: arquisoft/wiq_en2b/question-generator - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - workdir: questiongenerator - buildargs: | - DATABASE_USER - DATABASE_PASSWORD - deploy: - name: Deploy over SSH - runs-on: ubuntu-latest - needs: [docker-push-api, docker-push-webapp, docker-push-question-generator] - steps: - - name: Deploy over SSH - uses: fifsky/ssh-action@master - env: - API_URI: ${{ secrets.DEPLOY_HOST }} - DATABASE_USER: ${{ secrets.DATABASE_USER }} - DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - with: - host: ${{ secrets.DEPLOY_HOST }} - user: ${{ secrets.DEPLOY_USER }} - key: ${{ secrets.DEPLOY_KEY }} - command: | - wget https://raw.githubusercontent.com/arquisoft/wiq_en2b/master/docker-compose.yml -O docker-compose.yml - wget https://raw.githubusercontent.com/arquisoft/wiq_en2b/master/.env -O .env - echo "DATABASE_USER=${{ secrets.DATABASE_USER }}" >> .env - echo "DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }}" >> .env - echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env - echo "API_URI=http://${{ secrets.DEPLOY_HOST }}:8080" >> .env - docker compose --profile prod down - docker compose --profile prod up -d --pull always +name: Deploy on release + +on: + release: + types: [published] +jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: actions/checkout@v4 + - run: npm --prefix webapp ci + - run: npm --prefix webapp test -- --coverage + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - run: mvn clean verify + working-directory: api + env: + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + - name: Analyze with SonarCloud + uses: sonarsource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + e2e-tests: + needs: [ unit-tests ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm --prefix webapp install + - run: npm --prefix webapp run build + # - run: npm --prefix webapp run test:e2e + docker-push-api: + runs-on: ubuntu-latest + needs: [ e2e-tests ] + steps: + - uses: actions/checkout@v4 + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@v5 + env: + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + SSL_PASSWORD: ${{ secrets.SSL_PASSWORD }} + with: + name: arquisoft/wiq_en2b/api + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + workdir: api + buildargs: | + DATABASE_USER + DATABASE_PASSWORD + JWT_SECRET + SSL_PASSWORD + docker-push-kiwiq: + runs-on: ubuntu-latest + needs: [ e2e-tests ] + steps: + - uses: actions/checkout@v4 + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@v5 + with: + name: arquisoft/wiq_en2b/kiwiq + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + workdir: nginx_conf + docker-push-webapp: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [ e2e-tests ] + steps: + + - uses: actions/checkout@v4 + + - name: Create .env file + run: echo "REACT_APP_API_ENDPOINT=https://${{ secrets.DEPLOY_HOST }}:8443" > webapp/.env + + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@v5 + env: + REACT_APP_API_ENDPOINT: https://${{ secrets.DEPLOY_HOST }}:8443 + teamname: wiq_en2b + with: + name: arquisoft/wiq_en2b/webapp + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + workdir: webapp + buildargs: | + REACT_APP_API_ENDPOINT + docker-push-question-generator: + runs-on: ubuntu-latest + needs: [ e2e-tests ] + steps: + - uses: actions/checkout@v4 + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@v5 + env: + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + with: + name: arquisoft/wiq_en2b/question-generator + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + workdir: questiongenerator + buildargs: | + DATABASE_USER + DATABASE_PASSWORD + deploy: + name: Deploy over SSH + runs-on: ubuntu-latest + needs: [docker-push-api, docker-push-webapp, docker-push-question-generator, docker-push-kiwiq] + steps: + - name: Deploy over SSH + uses: fifsky/ssh-action@master + env: + API_URI: ${{ secrets.DEPLOY_HOST }} + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + with: + host: ${{ secrets.DEPLOY_HOST }} + user: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_KEY }} + command: | + wget https://raw.githubusercontent.com/arquisoft/wiq_en2b/master/docker-compose.yml -O docker-compose.yml + wget https://raw.githubusercontent.com/arquisoft/wiq_en2b/master/.env -O .env + echo "DATABASE_USER=${{ secrets.DATABASE_USER }}" >> .env + echo "DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }}" >> .env + echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env + echo "API_URI=https://${{ secrets.DEPLOY_HOST }}:8443" >> .env + echo "SSL_PASSWORD=${{ secrets.SSL_PASSWORD }}" >> .env + docker compose --profile prod down + docker compose --profile prod up -d --pull always diff --git a/.gitignore b/.gitignore index 66497da2..7bd85e8d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,8 @@ docs/build .idea .vscode .DS_Store + +*.crt +*.key +*.pem +.env \ No newline at end of file diff --git a/api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java b/api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java new file mode 100644 index 00000000..4e6c2886 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java @@ -0,0 +1,57 @@ +package lab.en2b.quizapi.commons.utils; + +import lab.en2b.quizapi.game.Game; +import lab.en2b.quizapi.game.GameMode; +import lab.en2b.quizapi.questions.question.QuestionCategory; + +import java.util.List; + +import static lab.en2b.quizapi.game.GameMode.KIWI_QUEST; + +public class GameModeUtils { + public static List getQuestionCategoriesForGamemode(GameMode gamemode, List questionCategoriesForCustom){ + if(gamemode == null){ + gamemode = KIWI_QUEST; + } + return switch (gamemode) { + case KIWI_QUEST -> List.of(QuestionCategory.ART, QuestionCategory.MUSIC, QuestionCategory.GEOGRAPHY); + case FOOTBALL_SHOWDOWN -> List.of(QuestionCategory.SPORTS); + case GEO_GENIUS -> List.of(QuestionCategory.GEOGRAPHY); + case VIDEOGAME_ADVENTURE -> List.of(QuestionCategory.VIDEOGAMES); + case ANCIENT_ODYSSEY -> List.of(QuestionCategory.MUSIC,QuestionCategory.ART); + case RANDOM -> List.of(QuestionCategory.values()); + case CUSTOM -> questionCategoriesForCustom; + }; + } + public static void setGamemodeParams(Game game){ + switch(game.getGamemode()){ + case KIWI_QUEST: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case FOOTBALL_SHOWDOWN: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case GEO_GENIUS: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case VIDEOGAME_ADVENTURE: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case ANCIENT_ODYSSEY: + game.setRounds(9L); + game.setRoundDuration(30); + break; + case RANDOM: + game.setRounds(9L); + game.setRoundDuration(30); + break; + default: + game.setRounds(9L); + game.setRoundDuration(30); + } + } +} diff --git a/api/src/main/java/lab/en2b/quizapi/game/Game.java b/api/src/main/java/lab/en2b/quizapi/game/Game.java index eec37592..19097219 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/Game.java +++ b/api/src/main/java/lab/en2b/quizapi/game/Game.java @@ -3,15 +3,19 @@ import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import lab.en2b.quizapi.commons.user.User; +import lab.en2b.quizapi.commons.utils.GameModeUtils; +import lab.en2b.quizapi.game.dtos.CustomGameDto; import lab.en2b.quizapi.questions.answer.Answer; import lab.en2b.quizapi.questions.question.Question; +import lab.en2b.quizapi.questions.question.QuestionCategory; import lombok.*; import java.time.Instant; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; +import java.util.ArrayList; import java.util.List; +import static lab.en2b.quizapi.game.GameMode.*; + @Entity @Table(name = "games") @NoArgsConstructor @@ -20,7 +24,6 @@ @Setter @Builder public class Game { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Setter(AccessLevel.NONE) @@ -35,7 +38,8 @@ public class Game { @NonNull private Integer roundDuration; private boolean currentQuestionAnswered; - + @Enumerated(EnumType.STRING) + private GameMode gamemode; @ManyToOne @NotNull @JoinColumn(name = "user_id") @@ -52,6 +56,19 @@ public class Game { @OrderColumn private List questions; private boolean isGameOver; + @Enumerated(EnumType.STRING) + private List questionCategoriesForCustom; + + public Game(User user,GameMode gamemode,String lang, CustomGameDto gameDto){ + this.user = user; + this.questions = new ArrayList<>(); + this.actualRound = 0L; + setLanguage(lang); + if(gamemode == CUSTOM) + setCustomGameMode(gameDto); + else + setGameMode(gamemode); + } public void newRound(Question question){ if(getActualRound() != 0){ @@ -110,16 +127,43 @@ public boolean answerQuestion(Long answerId){ return q.isCorrectAnswer(answerId); } public void setLanguage(String language){ + if(language == null){ + language = "en"; + } if(!isLanguageSupported(language)) throw new IllegalArgumentException("The language you provided is not supported"); this.language = language; } + public void setCustomGameMode(CustomGameDto gameDto){ + setRounds(gameDto.getRounds()); + setRoundDuration(gameDto.getRoundDuration()); + this.gamemode = CUSTOM; + setQuestionCategoriesForCustom(gameDto.getCategories()); + } + public void setGameMode(GameMode gamemode){ + if(gamemode == null){ + gamemode = KIWI_QUEST; + } + this.gamemode = gamemode; + GameModeUtils.setGamemodeParams(this); + } + public void setQuestionCategoriesForCustom(List questionCategoriesForCustom) { + if(gamemode != CUSTOM) + throw new IllegalStateException("You can't set custom categories for a non-custom gamemode!"); + if(questionCategoriesForCustom == null || questionCategoriesForCustom.isEmpty()) + throw new IllegalArgumentException("You can't set an empty list of categories for a custom gamemode!"); + this.questionCategoriesForCustom = questionCategoriesForCustom; + } + + public List getQuestionCategoriesForGamemode(){ + return GameModeUtils.getQuestionCategoriesForGamemode(gamemode,questionCategoriesForCustom); + } private boolean isLanguageSupported(String language) { return language.equals("en") || language.equals("es"); } public boolean shouldBeGameOver() { - return getActualRound() >= getRounds() && !isGameOver; + return getActualRound() >= getRounds() && !isGameOver && currentRoundIsOver(); } } diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameController.java b/api/src/main/java/lab/en2b/quizapi/game/GameController.java index c39409ae..08af1201 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -1,11 +1,12 @@ package lab.en2b.quizapi.game; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; -import lab.en2b.quizapi.game.dtos.AnswerGameResponseDto; -import lab.en2b.quizapi.game.dtos.GameAnswerDto; -import lab.en2b.quizapi.game.dtos.GameResponseDto; +import jakarta.validation.Valid; +import lab.en2b.quizapi.game.dtos.*; import lab.en2b.quizapi.questions.question.QuestionCategory; import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; import lombok.RequiredArgsConstructor; @@ -24,11 +25,19 @@ public class GameController { @Operation(summary = "Starts new game", description = "Requests the API to create a new game for a given authentication (a player)") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "400", description = "Given when: \n * language provided is not valid \n * gamemode provided is not valid \n * body is not provided with custom game", content = @io.swagger.v3.oas.annotations.media.Content), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) - @PostMapping("/new") - public ResponseEntity newGame(Authentication authentication){ - return ResponseEntity.ok(gameService.newGame(authentication)); + @Parameters({ + @Parameter(name = "lang", description = "The language of the game", example = "en"), + @Parameter(name = "gamemode", description = "The gamemode of the game", example = "KIWI_QUEST") + }) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The custom game dto, only required if the gamemode is CUSTOM") + @PostMapping("/play") + public ResponseEntity newGame(@RequestParam(required = false) String lang, @RequestParam(required=false) GameMode gamemode, @RequestBody(required = false) @Valid CustomGameDto customGameDto, Authentication authentication){ + if(gamemode == GameMode.CUSTOM && customGameDto == null) + throw new IllegalArgumentException("Custom game mode requires a body"); + return ResponseEntity.ok(gameService.newGame(lang,gamemode,customGameDto,authentication)); } @Operation(summary = "Starts a new round", description = "Starts the round (asks a question and its possible answers to the API and start the timer) for a given authentication (a player)") @@ -36,27 +45,30 @@ public ResponseEntity newGame(Authentication authentication){ @ApiResponse(responseCode = "200", description = "Successfully retrieved"), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) + @Parameter(name = "id", description = "The id of the game to start the round for", example = "1") @PostMapping("/{id}/startRound") public ResponseEntity startRound(@PathVariable Long id, Authentication authentication){ return ResponseEntity.ok(gameService.startRound(id, authentication)); } - @Operation(summary = "Starts a new round", description = "Gets the question and its possible answers from the API for a given authentication (a player)") + @Operation(summary = "Gets the current question", description = "Gets the question and its possible answers from the API for a given authentication (a player)") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved"), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) + @Parameter(name = "id", description = "The id of the game to get the current question for", example = "1") @GetMapping("/{id}/question") public ResponseEntity getCurrentQuestion(@PathVariable Long id, Authentication authentication){ return ResponseEntity.ok(gameService.getCurrentQuestion(id, authentication)); } - @Operation(summary = "Starts a new round", description = "Starts the round (getting a question and its possible answers and start the timer) for a given authentication (a player)") + @Operation(summary = "Answers the question", description = "Answers the question for the current game") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved"), @ApiResponse(responseCode = "400", description = "Not a valid answer", content = @io.swagger.v3.oas.annotations.media.Content), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) + @Parameter(name = "id", description = "The id of the game to answer the question for", example = "1") @PostMapping("/{id}/answer") public ResponseEntity answerQuestion(@PathVariable Long id, @RequestBody GameAnswerDto dto, Authentication authentication){ return ResponseEntity.ok(gameService.answerQuestion(id, dto, authentication)); @@ -65,9 +77,10 @@ public ResponseEntity answerQuestion(@PathVariable Long i @Operation(summary = "Changing languages", description = "Changes the language of the game for a given authentication (a player) and a language supported. Changes may are applied on the next round.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved"), - @ApiResponse(responseCode = "400", description = "Not a valid answer", content = @io.swagger.v3.oas.annotations.media.Content), + @ApiResponse(responseCode = "400", description = "Not a valid language to change to", content = @io.swagger.v3.oas.annotations.media.Content), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) + @Parameter(name = "id", description = "The id of the game to change the language for", example = "1") @PutMapping("/{id}/language") public ResponseEntity changeLanguage(@PathVariable Long id, @RequestParam String language, Authentication authentication){ return ResponseEntity.ok(gameService.changeLanguage(id, language, authentication)); @@ -78,12 +91,28 @@ public ResponseEntity changeLanguage(@PathVariable Long id, @Re @ApiResponse(responseCode = "200", description = "Successfully retrieved"), @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content), }) + @Parameter(name = "id", description = "The id of the game to get the summary for", example = "1") @GetMapping("/{id}/details") public ResponseEntity getGameDetails(@PathVariable Long id, Authentication authentication){ return ResponseEntity.ok(gameService.getGameDetails(id, authentication)); } - @GetMapping("/questionCategories") + @Operation(summary = "Get the list of gamemodes a game can have") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content) + }) + @GetMapping("/gamemodes") + public ResponseEntity> getQuestionGameModes(){ + return ResponseEntity.ok(gameService.getQuestionGameModes()); + } + + @Operation(summary = "Get the list of categories a game can have") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "403", description = "You are not logged in", content = @io.swagger.v3.oas.annotations.media.Content) + }) + @GetMapping("/question-categories") public ResponseEntity> getQuestionCategories(){ return ResponseEntity.ok(gameService.getQuestionCategories()); } diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameMode.java b/api/src/main/java/lab/en2b/quizapi/game/GameMode.java new file mode 100644 index 00000000..60e6bf7f --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/game/GameMode.java @@ -0,0 +1,12 @@ +package lab.en2b.quizapi.game; + +public enum GameMode { + KIWI_QUEST, + FOOTBALL_SHOWDOWN, + GEO_GENIUS, + VIDEOGAME_ADVENTURE, + ANCIENT_ODYSSEY, + RANDOM, + CUSTOM +} + diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameService.java b/api/src/main/java/lab/en2b/quizapi/game/GameService.java index ab75b519..9b084ef0 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -1,12 +1,9 @@ package lab.en2b.quizapi.game; import lab.en2b.quizapi.commons.user.UserService; -import lab.en2b.quizapi.game.dtos.AnswerGameResponseDto; -import lab.en2b.quizapi.game.dtos.GameAnswerDto; -import lab.en2b.quizapi.game.dtos.GameResponseDto; +import lab.en2b.quizapi.game.dtos.*; import lab.en2b.quizapi.game.mappers.GameResponseDtoMapper; import lab.en2b.quizapi.questions.question.QuestionCategory; -import lab.en2b.quizapi.questions.question.QuestionRepository; import lab.en2b.quizapi.questions.question.QuestionService; import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; @@ -17,7 +14,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -29,81 +25,110 @@ public class GameService { private final GameResponseDtoMapper gameResponseDtoMapper; private final UserService userService; private final QuestionService questionService; - private final QuestionRepository questionRepository; private final QuestionResponseDtoMapper questionResponseDtoMapper; private final StatisticsRepository statisticsRepository; + /** + * Creates a new game for the user + * @param lang the language of the game, default is ENGLISH + * @param gamemode the gamemode of the game, default is KIWI_QUEST + * @param newGameDto the custom game dto, only required if the gamemode is CUSTOM + * @param authentication the authentication of the user + * @return the newly created game + */ @Transactional - public GameResponseDto newGame(Authentication authentication) { + public GameResponseDto newGame(String lang, GameMode gamemode, CustomGameDto newGameDto, Authentication authentication) { + // Check if there is an active game for the user Optional game = gameRepository.findActiveGameForUser(userService.getUserByAuthentication(authentication).getId()); - - if (game.isPresent()){ - if (game.get().shouldBeGameOver()){ - game.get().setGameOver(true); - gameRepository.save(game.get()); - saveStatistics(game.get()); - }else{ - return gameResponseDtoMapper.apply(game.get()); - } + if (game.isPresent() && !wasGameMeantToBeOver(game.get())){ + // If there is an active game and it should not be over, return it + return gameResponseDtoMapper.apply(game.get()); } - return gameResponseDtoMapper.apply(gameRepository.save(Game.builder() - .user(userService.getUserByAuthentication(authentication)) - .questions(new ArrayList<>()) - .rounds(9L) - .actualRound(0L) - .correctlyAnsweredQuestions(0L) - .roundDuration(30) - .language("en") - .build())); + return gameResponseDtoMapper.apply(gameRepository.save( + new Game(userService.getUserByAuthentication(authentication),gamemode,lang,newGameDto) + )); } + /** + * Starts a new round for the game + * @param id the id of the game to start the round for + * @param authentication the authentication of the user + * @return the game with the new round started + */ + @Transactional public GameResponseDto startRound(Long id, Authentication authentication) { + // Get the game by id and user Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); - if (game.shouldBeGameOver()){ - game.setGameOver(true); - gameRepository.save(game); - saveStatistics(game); - } - game.newRound(questionService.findRandomQuestion(game.getLanguage())); + // Check if the game should be over + wasGameMeantToBeOver(game); + // Start a new round + game.newRound(questionService.findRandomQuestion(game.getLanguage(),game.getQuestionCategoriesForGamemode())); return gameResponseDtoMapper.apply(gameRepository.save(game)); } + /** + * Gets the current question for the game + * @param id the id of the game to get the question for + * @param authentication the authentication of the user + * @return the current question + */ public QuestionResponseDto getCurrentQuestion(Long id, Authentication authentication){ Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); return questionResponseDtoMapper.apply(game.getCurrentQuestion()); } + /** + * Answers the current question for the game + * @param id the id of the game to answer the question for + * @param dto the answer dto + * @param authentication the authentication of the user + * @return the response of the answer + */ @Transactional public AnswerGameResponseDto answerQuestion(Long id, GameAnswerDto dto, Authentication authentication){ Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); + // Check if the game should be over + wasGameMeantToBeOver(game); + // Answer the question boolean wasCorrect = game.answerQuestion(dto.getAnswerId()); - - if (game.shouldBeGameOver()){ - game.setGameOver(true); - gameRepository.save(game); - saveStatistics(game); - } + // Check if the game is over after the answer + wasGameMeantToBeOver(game); return new AnswerGameResponseDto(wasCorrect); } + + /** + * Saves the statistics of the game + * @param game the game to save the statistics for + */ private void saveStatistics(Game game){ + Statistics statistics; if (statisticsRepository.findByUserId(game.getUser().getId()).isPresent()){ - Statistics statistics = statisticsRepository.findByUserId(game.getUser().getId()).get(); + // If there are statistics for the user, update them + statistics = statisticsRepository.findByUserId(game.getUser().getId()).get(); statistics.updateStatistics(game.getCorrectlyAnsweredQuestions(), game.getQuestions().size()-game.getCorrectlyAnsweredQuestions(), game.getRounds()); - statisticsRepository.save(statistics); } else { - Statistics statistics = Statistics.builder() + // If there are no statistics for the user, create new ones + statistics = Statistics.builder() .user(game.getUser()) .correct(game.getCorrectlyAnsweredQuestions()) .wrong(game.getQuestions().size()-game.getCorrectlyAnsweredQuestions()) .total(game.getRounds()) .build(); - statisticsRepository.save(statistics); } + statisticsRepository.save(statistics); } + + /** + * Changes the language of the game. The game language will only change after the next round. + * @param id the id of the game to change the language for + * @param language the language to change to + * @param authentication the authentication of the user + * @return the game with the new language + */ public GameResponseDto changeLanguage(Long id, String language, Authentication authentication) { Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); if(game.isGameOver()){ @@ -113,17 +138,45 @@ public GameResponseDto changeLanguage(Long id, String language, Authentication a return gameResponseDtoMapper.apply(gameRepository.save(game)); } + /** + * Gets the game details + * @param id the id of the game to get the details for + * @param authentication the authentication of the user + * @return the game details + */ public GameResponseDto getGameDetails(Long id, Authentication authentication) { Game game = gameRepository.findByIdForUser(id, userService.getUserByAuthentication(authentication).getId()).orElseThrow(); + wasGameMeantToBeOver(game); + return gameResponseDtoMapper.apply(game); + } + + public List getQuestionCategories() { + return Arrays.asList(QuestionCategory.values()); + } + + private boolean wasGameMeantToBeOver(Game game) { if (game.shouldBeGameOver()){ game.setGameOver(true); gameRepository.save(game); saveStatistics(game); + return true; } - return gameResponseDtoMapper.apply(game); + return false; } - public List getQuestionCategories() { - return Arrays.asList(QuestionCategory.values()); + /** + * Gets the list of gamemodes a game can have + * @return the list of gamemodes + */ + public List getQuestionGameModes() { + return List.of( + new GameModeDto("Kiwi Quest","Our curated selection of the most exquisite questions. Enjoy with a glass of wine",GameMode.KIWI_QUEST,"FaKiwiBird"), + new GameModeDto("Football Showdown","Like sports? Like balls? This gamemode is for you!",GameMode.FOOTBALL_SHOWDOWN,"IoIosFootball"), + new GameModeDto("Geo Genius","Do you know the capital of Mongolia? I don't, so if you do this game is for you!",GameMode.GEO_GENIUS,"FaGlobeAmericas"), + new GameModeDto("Videogame Adventure","It's dangerous to go alone, guess this!",GameMode.VIDEOGAME_ADVENTURE,"IoLogoGameControllerB"), + new GameModeDto("Ancient Odyssey","Antiques are pricey for a reason!",GameMode.ANCIENT_ODYSSEY,"FaPalette"), + new GameModeDto("Random","Try a bit of everything!",GameMode.RANDOM,"FaRandom"), + new GameModeDto("Custom","Don't like our gamemodes? That's fine! (I only feel a bit offended)",GameMode.CUSTOM,"FaCog") + ); } } diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/CustomGameDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/CustomGameDto.java new file mode 100644 index 00000000..4dd9fa22 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/CustomGameDto.java @@ -0,0 +1,33 @@ +package lab.en2b.quizapi.game.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lab.en2b.quizapi.questions.question.QuestionCategory; +import lombok.*; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Setter +public class CustomGameDto { + @Positive + @NotNull + @NonNull + @Schema(description = "Number of rounds for the custom game",example = "9") + private Long rounds; + @Positive + @NotNull + @NonNull + @JsonProperty("round_duration") + @Schema(description = "Duration of the round in seconds",example = "30") + private Integer roundDuration; + @NotNull + @NonNull + @Schema(description = "Categories selected for questions",example = "[\"HISTORY\",\"SCIENCE\"]") + private List categories; +} diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java new file mode 100644 index 00000000..82550eba --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameModeDto.java @@ -0,0 +1,24 @@ +package lab.en2b.quizapi.game.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lab.en2b.quizapi.game.GameMode; +import lombok.*; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Setter +public class GameModeDto { + @Schema(description = "Beautified name of the game mode",example = "Quiwi Quest") + private String name; + @Schema(description = "Description of the game mode",example = "Test description of the game mode") + private String description; + @JsonProperty("internal_representation") + @Schema(description = "Internal code used for describing the game mode",example = "KIWI_QUEST") + private GameMode internalRepresentation; + @JsonProperty("icon_name") + @Schema(description = "Code for the icon used in the frontend of the application",example = "FaKiwiBird") + private String iconName; +} diff --git a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java index e4af2e5f..c3b70dad 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/game/dtos/GameResponseDto.java @@ -2,7 +2,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; -import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; +import lab.en2b.quizapi.commons.user.UserResponseDto; +import lab.en2b.quizapi.game.GameMode; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -40,4 +41,7 @@ public class GameResponseDto { @Schema(description = "Whether the game has finished or not", example = "true") private boolean isGameOver; + + @Schema(description = "Game mode for the game", example = "KIWI_QUEST") + private GameMode gamemode; } diff --git a/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java b/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java index 4f061c70..3fbc0b0f 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java +++ b/api/src/main/java/lab/en2b/quizapi/game/mappers/GameResponseDtoMapper.java @@ -23,6 +23,7 @@ public GameResponseDto apply(Game game) { .actualRound(game.getActualRound()) .roundDuration(game.getRoundDuration()) .roundStartTime(game.getRoundStartTime() != null? Instant.ofEpochMilli(game.getRoundStartTime()).toString(): null) + .gamemode(game.getGamemode()) .isGameOver(game.isGameOver()) .build(); } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java b/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java index 50298f7a..9e8154dc 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java @@ -1,6 +1,6 @@ package lab.en2b.quizapi.questions.answer; public enum AnswerCategory { - CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR + CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR, GAMES_PUBLISHER, PAINTING } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionCategory.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionCategory.java index 147ed65d..d97db494 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionCategory.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionCategory.java @@ -2,5 +2,5 @@ public enum QuestionCategory { //HISTORY, GEOGRAPHY, SCIENCE, MATH, LITERATURE, ART, SPORTS, MUSIC, MOVIES, TV, POLITICS, OTHER - GEOGRAPHY, SPORTS, MUSIC + GEOGRAPHY, SPORTS, MUSIC, ART, VIDEOGAMES } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java index 35c61d92..fd8e2eca 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionRepository.java @@ -3,7 +3,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.List; + public interface QuestionRepository extends JpaRepository { - @Query(value = "SELECT q.* FROM questions q INNER JOIN answers a ON q.correct_answer_id=a.id WHERE a.language = ?1 ORDER BY RANDOM() LIMIT 1", nativeQuery = true) - Question findRandomQuestion(String lang); + @Query(value = "SELECT q.* FROM questions q INNER JOIN answers a ON q.correct_answer_id=a.id WHERE a.language = ?1 " + + "AND q.question_category IN ?2 " + + " ORDER BY RANDOM() LIMIT 1 ", nativeQuery = true) + Question findRandomQuestion(String lang, List questionCategories); } diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java index 264e3605..47a8e549 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionService.java @@ -42,19 +42,20 @@ else if(question.getAnswers().stream().noneMatch(i -> i.getId().equals(answerDto } public QuestionResponseDto getRandomQuestion(String lang) { - return questionResponseDtoMapper.apply(findRandomQuestion(lang)); + return questionResponseDtoMapper.apply(findRandomQuestion(lang, List.of(QuestionCategory.values()))); } /** * Find a random question for the specified language - * @param lang The language to find the question for + * @param language The language to find the question for * @return The random question */ - public Question findRandomQuestion(String lang){ - if (lang==null || lang.isBlank()) { - lang = "en"; + + public Question findRandomQuestion(String language, List questionCategoriesForCustom) { + if (language==null || language.isBlank()) { + language = "en"; } - Question q = questionRepository.findRandomQuestion(lang); + Question q = questionRepository.findRandomQuestion(language,questionCategoriesForCustom.stream().map(Enum::toString).toList()); if(q==null) { throw new InternalApiErrorException("No questions found for the specified language!"); } @@ -74,6 +75,9 @@ public QuestionResponseDto getQuestionById(Long id) { //TODO: CHAPUZAS, FIXEAR ESTO private void loadAnswers(Question question) { // Create the new answers list with the distractors + if(question.getAnswers().size() > 1) { + return; + } List answers = new ArrayList<>(QuestionHelper.getDistractors(answerRepository, question)); // Add the correct answers.add(question.getCorrectAnswer()); @@ -84,4 +88,5 @@ private void loadAnswers(Question question) { question.setAnswers(answers); questionRepository.save(question); } + } diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index a2042204..e129b09c 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -10,3 +10,9 @@ springdoc.api-docs.path=/swagger/api-docs management.endpoints.web.exposure.include=prometheus management.endpoint.prometheus.enabled=true + +server.port=8443 +server.ssl.key-alias=tomcat +server.ssl.key-store=/etc/letsencrypt/live/kiwiq.run.place/keystore.p12 +server.ssl.key-store-type=PKCS12 +server.ssl.key-store-password=${SSL_PASSWORD} \ No newline at end of file diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java index f9865ad8..d570e709 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameControllerTest.java @@ -3,9 +3,8 @@ import lab.en2b.quizapi.auth.config.SecurityConfig; import lab.en2b.quizapi.auth.jwt.JwtUtils; import lab.en2b.quizapi.commons.user.UserService; +import lab.en2b.quizapi.game.dtos.CustomGameDto; import lab.en2b.quizapi.game.dtos.GameAnswerDto; -import lab.en2b.quizapi.questions.answer.dtos.AnswerDto; -import lab.en2b.quizapi.questions.question.QuestionController; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -41,20 +40,45 @@ public class GameControllerTest { @Test void newQuestionShouldReturn403() throws Exception{ - mockMvc.perform(post("/games/new") + mockMvc.perform(post("/games/play") .contentType("application/json") .with(csrf())) .andExpect(status().isForbidden()); } @Test - void newQuestionShouldReturn200() throws Exception{ - mockMvc.perform(post("/games/new") + void newGameShouldReturn200() throws Exception{ + mockMvc.perform(post("/games/play") .with(user("test").roles("user")) .contentType("application/json") .with(csrf())) .andExpect(status().isOk()); } + @Test + void newGameCustomNoBodyShouldReturn400() throws Exception{ + mockMvc.perform(post("/games/play?gamemode=CUSTOM") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isBadRequest()); + } + @Test + void newGameInvalidBodyForCustomShouldReturn400() throws Exception{ + mockMvc.perform(post("/games/play?gamemode=CUSTOM") + .with(user("test").roles("user")) + .content(asJsonString(new CustomGameDto())) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isBadRequest()); + } + @Test + void newGameInvalidGameModeShouldReturn400() throws Exception{ + mockMvc.perform(post("/games/play?gamemode=patata") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isBadRequest()); + } @Test void startRoundShouldReturn403() throws Exception{ @@ -163,7 +187,7 @@ void getGameDetailsShouldReturn200() throws Exception{ @Test void getQuestionCategoriesShouldReturn200() throws Exception{ - mockMvc.perform(get("/games/questionCategories") + mockMvc.perform(get("/games/question-categories") .with(user("test").roles("user")) .contentType("application/json") .with(csrf())) @@ -172,10 +196,28 @@ void getQuestionCategoriesShouldReturn200() throws Exception{ @Test void getQuestionCategoriesShouldReturn403() throws Exception{ - mockMvc.perform(get("/games/questionCategories") + mockMvc.perform(get("/games/question-categories") + .contentType("application/json") + .with(csrf())) + .andExpect(status().isForbidden()); + } + + @Test + void getGameModeshouldReturn200() throws Exception{ + mockMvc.perform(get("/games/gamemodes") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void getGameModesShouldReturn403() throws Exception{ + mockMvc.perform(get("/games/gamemodes") .contentType("application/json") .with(csrf())) .andExpect(status().isForbidden()); } + } diff --git a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java index 1c84032e..b9c4b9d4 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -5,6 +5,7 @@ import lab.en2b.quizapi.commons.user.dtos.UserResponseDto; import lab.en2b.quizapi.commons.user.UserService; import lab.en2b.quizapi.commons.user.mappers.UserResponseDtoMapper; +import lab.en2b.quizapi.game.dtos.CustomGameDto; import lab.en2b.quizapi.game.dtos.GameAnswerDto; import lab.en2b.quizapi.game.dtos.GameResponseDto; import lab.en2b.quizapi.game.mappers.GameResponseDtoMapper; @@ -14,6 +15,7 @@ import lab.en2b.quizapi.questions.question.*; import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; +import lab.en2b.quizapi.statistics.Statistics; import lab.en2b.quizapi.statistics.StatisticsRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,8 +32,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith({MockitoExtension.class, SpringExtension.class}) public class GameServiceTest { @@ -45,9 +46,6 @@ public class GameServiceTest { @Mock private GameRepository gameRepository; - @Mock - private QuestionRepository questionRepository; - @Mock private StatisticsRepository statisticsRepository; @@ -73,7 +71,7 @@ public class GameServiceTest { @BeforeEach void setUp() { this.questionResponseDtoMapper = new QuestionResponseDtoMapper(); - this.gameService = new GameService(gameRepository,new GameResponseDtoMapper(new UserResponseDtoMapper()), userService, questionService, questionRepository, questionResponseDtoMapper, statisticsRepository); + this.gameService = new GameService(gameRepository,new GameResponseDtoMapper(new UserResponseDtoMapper()), userService, questionService, questionResponseDtoMapper, statisticsRepository); this.defaultUser = User.builder() .id(1L) .email("test@email.com") @@ -135,8 +133,10 @@ void setUp() { .user(defaultUserResponseDto) .rounds(9L) .correctlyAnsweredQuestions(0L) + .roundStartTime(Instant.ofEpochSecond(0L).toString()) .actualRound(0L) .roundDuration(30) + .gamemode(GameMode.KIWI_QUEST) .build(); this.defaultGame = Game.builder() .id(1L) @@ -145,27 +145,85 @@ void setUp() { .rounds(9L) .actualRound(0L) .roundStartTime(0L) + .gamemode(GameMode.KIWI_QUEST) .correctlyAnsweredQuestions(0L) .language("en") .roundDuration(30) .build(); } + // NEW GAME TESTS @Test public void newGame(){ Authentication authentication = mock(Authentication.class); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - GameResponseDto gameDto = gameService.newGame(authentication); + GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); + assertEquals(defaultGameResponseDto, gameDto); + } + @Test + public void newGameActiveGame(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); + GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); + defaultGameResponseDto.setId(1L); assertEquals(defaultGameResponseDto, gameDto); } + @Test + public void newGameWasMeantToBeOver(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + defaultGame.setActualRound(10L); + gameService.newGame(null,null,null,authentication); + verify(statisticsRepository, times(1)).save(any()); + } + + @Test + public void newGameWasMeantToBeOverExistingLeaderboard(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.findActiveGameForUser(1L)).thenReturn(Optional.of(defaultGame)); + when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.of(Statistics.builder().user(new User()) + .correct(0L) + .wrong(0L) + .total(0L) + .build())); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + defaultGame.setActualRound(10L); + gameService.newGame(null,null,null,authentication); + verify(statisticsRepository, times(1)).save(any()); + } + + @Test + public void newGameCustomGame(){ + Authentication authentication = mock(Authentication.class); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + GameResponseDto gameDto = gameService.newGame("es",GameMode.CUSTOM, + CustomGameDto.builder() + .roundDuration(30) + .categories(List.of(QuestionCategory.GEOGRAPHY)) + .rounds(10L) + .build() + ,authentication); + defaultGameResponseDto.setGamemode(GameMode.CUSTOM); + defaultGameResponseDto.setRounds(10L); + defaultGameResponseDto.setRoundDuration(30); + + assertEquals(defaultGameResponseDto, gameDto); + } + + // START ROUND TESTS @Test public void startRound(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); GameResponseDto gameDto = gameService.startRound(1L, authentication); GameResponseDto result = defaultGameResponseDto; @@ -178,47 +236,45 @@ public void startRound(){ @Test public void startRoundGameOver(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); defaultGame.setActualRound(10L); assertThrows(IllegalStateException.class, () -> gameService.startRound(1L,authentication)); } - /** @Test public void startRoundWhenRoundNotFinished(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); gameService.startRound(1L,authentication); assertThrows(IllegalStateException.class, () -> gameService.startRound(1L,authentication)); } - **/ @Test public void getCurrentQuestion() { when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); gameService.startRound(1L,authentication); QuestionResponseDto questionDto = gameService.getCurrentQuestion(1L,authentication); assertEquals(defaultQuestionResponseDto, questionDto); } - /*@Test + @Test public void getCurrentQuestionRoundNotStarted() { when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); assertThrows(IllegalStateException.class, () -> gameService.getCurrentQuestion(1L,authentication)); - }*/ + } @Test public void getCurrentQuestionRoundFinished() { when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); gameService.startRound(1L,authentication); defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); @@ -230,7 +286,7 @@ public void getCurrentQuestionGameFinished() { when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); gameService.startRound(1L,authentication); defaultGame.setGameOver(true); defaultGame.setActualRound(10L); @@ -242,8 +298,8 @@ public void answerQuestionCorrectly(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); - gameService.newGame(authentication); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication); gameService.getGameDetails(1L, authentication); @@ -256,8 +312,8 @@ public void answerQuestionIncorrectly(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); - gameService.newGame(authentication); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); gameService.answerQuestion(1L, new GameAnswerDto(2L), authentication); gameService.getGameDetails(1L, authentication); @@ -270,21 +326,34 @@ public void answerQuestionWhenGameHasFinished(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); - gameService.newGame(authentication); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); defaultGame.setGameOver(true); defaultGame.setActualRound(30L); assertThrows(IllegalStateException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication)); } + @Test + public void answerQuestionLastRound(){ + when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); + when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); + defaultGame.setActualRound(8L); + gameService.startRound(1L, authentication); + gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication); + verify(statisticsRepository, times(1)).save(any()); + } + @Test public void answerQuestionWhenRoundHasFinished(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); - gameService.newGame(authentication); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); defaultGame.setRoundStartTime(Instant.now().minusSeconds(100).toEpochMilli()); assertThrows(IllegalStateException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(1L), authentication)); @@ -295,8 +364,8 @@ public void answerQuestionInvalidId(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - when(questionService.findRandomQuestion(any())).thenReturn(defaultQuestion); - gameService.newGame(authentication); + when(questionService.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); assertThrows(IllegalArgumentException.class, () -> gameService.answerQuestion(1L, new GameAnswerDto(3L), authentication)); } @@ -306,7 +375,7 @@ public void changeLanguage(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); gameService.changeLanguage(1L, "es", authentication); gameService.getGameDetails(1L, authentication); @@ -319,7 +388,7 @@ public void changeLanguageGameOver(){ when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); defaultGame.setGameOver(true); defaultGame.setActualRound(10L); @@ -332,7 +401,7 @@ public void changeLanguageInvalidLanguage(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); assertThrows(IllegalArgumentException.class, () -> gameService.changeLanguage(1L, "patata", authentication)); } @@ -341,9 +410,11 @@ public void getGameDetails(){ when(gameRepository.findByIdForUser(any(), any())).thenReturn(Optional.of(defaultGame)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - GameResponseDto gameDto = gameService.newGame(authentication); + + GameResponseDto gameDto = gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); gameService.getGameDetails(1L, authentication); + assertEquals(defaultGameResponseDto, gameDto); } @@ -352,7 +423,7 @@ public void getGameDetailsInvalidId(){ when(gameRepository.findByIdForUser(1L, 1L)).thenReturn(Optional.of(defaultGame)); when(userService.getUserByAuthentication(authentication)).thenReturn(defaultUser); when(gameRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); - gameService.newGame(authentication); + gameService.newGame(null,null,null,authentication); gameService.startRound(1L, authentication); assertThrows(NoSuchElementException.class, () -> gameService.getGameDetails(2L, authentication)); } diff --git a/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java b/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java index 347790a4..a46ed90d 100644 --- a/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java @@ -1,5 +1,6 @@ package lab.en2b.quizapi.questions; +import lab.en2b.quizapi.commons.exceptions.InternalApiErrorException; import lab.en2b.quizapi.questions.answer.Answer; import lab.en2b.quizapi.questions.answer.AnswerCategory; import lab.en2b.quizapi.questions.answer.AnswerRepository; @@ -98,11 +99,23 @@ void setUp() { @Test void testGetRandomQuestion() { - when(questionRepository.findRandomQuestion("en")).thenReturn(defaultQuestion); + when(questionRepository.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); QuestionResponseDto response = questionService.getRandomQuestion(""); assertEquals(response.getId(), defaultResponseDto.getId()); } + @Test + void testGetRandomQuestionAnswersNotYetLoaded() { + when(questionRepository.findRandomQuestion(any(),any())).thenReturn(defaultQuestion); + defaultQuestion.setAnswers(List.of()); + QuestionResponseDto response = questionService.getRandomQuestion(""); + + assertEquals(response.getId(), defaultResponseDto.getId()); + } + @Test + void testGetRandomQuestionNoQuestionsFound() { + assertThrows(InternalApiErrorException.class,() -> questionService.getRandomQuestion("")); + } @Test void testGetQuestionById(){ diff --git a/docker-compose.yml b/docker-compose.yml index 2900cafd..b4a30ecb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,112 +1,124 @@ -version: '3' -services: - WIQ_DB: - container_name: postgresql-${teamname:-defaultASW} - environment: - POSTGRES_USER: ${DATABASE_USER} - POSTGRES_PASSWORD: ${DATABASE_PASSWORD} - volumes: - - postgres_data:/var/lib/postgresql/data - image: postgres:latest - profiles: ["dev", "prod"] - networks: - - mynetwork - ports: - - "5432:5432" - - api: - container_name: api-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/api:latest - profiles: [ "dev", "prod" ] - build: - context: ./api - args: - DATABASE_USER: ${DATABASE_USER} - DATABASE_PASSWORD: ${DATABASE_PASSWORD} - JWT_SECRET: ${JWT_SECRET} - environment: - - DATABASE_URL=jdbc:postgresql://WIQ_DB:5432/wiq - - DATABASE_USER=${DATABASE_USER} - - DATABASE_PASSWORD=${DATABASE_PASSWORD} - - JWT_SECRET=${JWT_SECRET} - networks: - - mynetwork - depends_on: + version: '3' + services: + WIQ_DB: + container_name: postgresql-${teamname:-defaultASW} + environment: + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + image: postgres:latest + profiles: ["dev", "prod"] + networks: + mynetwork: + + api: + container_name: api-${teamname:-defaultASW} + image: api:latest + profiles: ["dev", "prod"] + build: + context: ./api + args: + DATABASE_USER: ${DATABASE_USER} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + JWT_SECRET: ${JWT_SECRET} + SSL_PASSWORD: ${SSL_PASSWORD} + environment: + - DATABASE_URL=jdbc:postgresql://WIQ_DB:5432/wiq + - DATABASE_USER=${DATABASE_USER} + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - JWT_SECRET=${JWT_SECRET} + - SSL_PASSWORD=${SSL_PASSWORD} + ports: + - 8443:8443 + networks: + mynetwork: + volumes: + - /certs:/etc/letsencrypt/live/kiwiq.run.place:ro + depends_on: - WIQ_DB - ports: - - "8080:8080" + - kiwiq - question-generator: - container_name: question-generator-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/question-generator:latest - profiles: [ "dev", "prod" ] - build: - context: ./questiongenerator - args: - DATABASE_USER: ${DATABASE_USER} - DATABASE_PASSWORD: ${DATABASE_PASSWORD} - environment: - - DATABASE_URL=jdbc:postgresql://WIQ_DB:5432/wiq - - DATABASE_USER=${DATABASE_USER} - - DATABASE_PASSWORD=${DATABASE_PASSWORD} - networks: - - mynetwork - depends_on: - - WIQ_DB + question-generator: + container_name: question-generator-${teamname:-defaultASW} + image: ghcr.io/arquisoft/wiq_en2b/question-generator:latest + profiles: ["dev", "prod"] + build: + context: ./questiongenerator + args: + DATABASE_USER: ${DATABASE_USER} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + environment: + - DATABASE_URL=jdbc:postgresql://WIQ_DB:5432/wiq + - DATABASE_USER=${DATABASE_USER} + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + networks: + mynetwork: + depends_on: + - WIQ_DB - webapp: - container_name: webapp-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/webapp:latest - profiles: [ "dev", "prod" ] - build: - args: - REACT_APP_API_ENDPOINT: ${API_URI} - context: ./webapp - environment: - - REACT_APP_API_ENDPOINT=${API_URI} - ports: - - "80:3000" - - prometheus: - image: prom/prometheus - container_name: prometheus-${teamname:-defaultASW} - profiles: ["dev", "prod"] - networks: - - mynetwork - volumes: - - ./quiz-api/monitoring/prometheus:/etc/prometheus - - prometheus_data:/prometheus - ports: - - "9090:9090" - depends_on: - - api - - grafana: - image: grafana/grafana - container_name: grafana-${teamname:-defaultASW} - profiles: [ "dev" , "prod"] - networks: - - mynetwork - volumes: - - grafana_data:/var/lib/grafana - - ./quiz-api/monitoring/grafana/provisioning:/etc/grafana/provisioning - environment: - - GF_SERVER_HTTP_PORT=9091 - - GF_AUTH_DISABLE_LOGIN_FORM=true - - GF_AUTH_ANONYMOUS_ENABLED=true - - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - ports: - - "9091:9091" - depends_on: - - prometheus + prometheus: + image: prom/prometheus + container_name: prometheus-${teamname:-defaultASW} + profiles: ["dev", "prod"] + networks: + mynetwork: + volumes: + - ./quiz-api/monitoring/prometheus:/etc/prometheus + - prometheus_data:/prometheus + - /certs:/etc/letsencrypt/kiwiq.run.place:ro + depends_on: + - api + kiwiq: + image: ghcr.io/arquisoft/wiq_en2b/kiwiq:latest + container_name: kiwiq + networks: + mynetwork: + links: + - webapp + ports: + - "443:443" + depends_on: + - webapp + volumes: + - /certs:/etc/letsencrypt/live/kiwiq.run.place:ro + build: + context: ./nginx_conf -volumes: - postgres_data: - prometheus_data: - grafana_data: + webapp: + container_name: webapp-${teamname:-defaultASW} + image: ghcr.io/arquisoft/wiq_en2b/webapp:latest + profiles: [ "dev", "prod" ] + build: + args: + REACT_APP_API_ENDPOINT: ${API_URI} + context: ./webapp + environment: + - REACT_APP_API_ENDPOINT=${API_URI} + networks: + mynetwork: + grafana: + image: grafana/grafana + container_name: grafana-${teamname:-defaultASW} + profiles: [ "dev" , "prod"] + networks: + mynetwork: + volumes: + - grafana_data:/var/lib/grafana + - ./quiz-api/monitoring/grafana/provisioning:/etc/grafana/provisioning + environment: + - GF_SERVER_HTTP_PORT=9091 + - GF_AUTH_DISABLE_LOGIN_FORM=true + depends_on: + - prometheus + volumes: + postgres_data: + prometheus_data: + grafana_data: + certs: -networks: - mynetwork: - driver: bridge + networks: + mynetwork: + driver: bridge diff --git a/proxy_conf/Dockerfile b/proxy_conf/Dockerfile new file mode 100644 index 00000000..3dd8b7f0 --- /dev/null +++ b/proxy_conf/Dockerfile @@ -0,0 +1,5 @@ +FROM nginx:bookworm + +COPY ./config.conf /etc/nginx/nginx.conf + +RUN apt update -y && apt upgrade -y && apt install -y openssl && openssl dhparam -out /etc/nginx/dhparams.pem 2048 \ No newline at end of file diff --git a/proxy_conf/config.conf b/proxy_conf/config.conf new file mode 100644 index 00000000..f4aa43bd --- /dev/null +++ b/proxy_conf/config.conf @@ -0,0 +1,47 @@ +events {} + +http { + + server_tokens off; + + map $http_authorization $auth_header { + default ""; + "~^(.*)" $1; + } + + proxy_set_header Authorization $auth_header; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + + server { + listen 80 default_server; + listen [::]:80 default_server; + + location / { + proxy_pass http://webapp:3000; + } + } + + server { + http2 on; + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + ssl_certificate /etc/letsencrypt/live/kiwiq.run.place/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/kiwiq.run.place/privkey.pem; + ssl_dhparam /etc/nginx/dhparams.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; + ssl_session_timeout 10m; + ssl_session_cache shared:SSL:10m; + + location / { + proxy_pass http://webapp:3000; + } + } +} diff --git a/questiongenerator/src/main/java/Main.java b/questiongenerator/src/main/java/Main.java index 3ce4b779..8c0b5755 100644 --- a/questiongenerator/src/main/java/Main.java +++ b/questiongenerator/src/main/java/Main.java @@ -2,30 +2,48 @@ import repositories.GeneralRepositoryStorer; import templates.BallonDOrQuestion; import templates.CountryCapitalQuestion; +import templates.VideogamesPublisherQuestion; public class Main { public static void main(String[] args) { + // TEXT if(GeneralRepositoryStorer.doesntExist(AnswerCategory.CAPITAL_CITY)) { new CountryCapitalQuestion("en"); new CountryCapitalQuestion("es"); } - /* - if(GeneralRepositoryStorer.doesntExist(AnswerCategory.SONG.toString())) { - new SongQuestion("en"); - new SongQuestion("es"); + if (GeneralRepositoryStorer.doesntExist(AnswerCategory.BALLON_DOR)) { + new BallonDOrQuestion(""); // No need to specify language code as it is not used } + if (GeneralRepositoryStorer.doesntExist(AnswerCategory.GAMES_PUBLISHER)) { + new VideogamesPublisherQuestion("en"); + new VideogamesPublisherQuestion("es"); + } + + + /* + // IMAGES + if(GeneralRepositoryStorer.doesntExist(AnswerCategory.STADIUM.toString())) { new StadiumQuestion("en"); new StadiumQuestion("es"); } - */ - if (GeneralRepositoryStorer.doesntExist(AnswerCategory.BALLON_DOR)) { - new BallonDOrQuestion(""); // No need to specify language code as it is not used + if (GeneralRepositoryStorer.doesntExist(AnswerCategory.PAINTING)) { + new PaintingQuestion("en"); + new PaintingQuestion("es"); } + */ + + /* + // VIDEOS + if(GeneralRepositoryStorer.doesntExist(AnswerCategory.SONG.toString())) { + new SongQuestion("en"); + new SongQuestion("es"); + } + */ } } \ No newline at end of file diff --git a/questiongenerator/src/main/java/model/AnswerCategory.java b/questiongenerator/src/main/java/model/AnswerCategory.java index 1fc9197b..9af4f704 100644 --- a/questiongenerator/src/main/java/model/AnswerCategory.java +++ b/questiongenerator/src/main/java/model/AnswerCategory.java @@ -1,6 +1,6 @@ package model; public enum AnswerCategory { - CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR + CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR, GAMES_PUBLISHER, PAINTING } diff --git a/questiongenerator/src/main/java/model/QuestionCategory.java b/questiongenerator/src/main/java/model/QuestionCategory.java index 47f0782c..d39d26dc 100644 --- a/questiongenerator/src/main/java/model/QuestionCategory.java +++ b/questiongenerator/src/main/java/model/QuestionCategory.java @@ -2,5 +2,5 @@ public enum QuestionCategory { //HISTORY, GEOGRAPHY, SCIENCE, MATH, LITERATURE, ART, SPORTS, MUSIC, MOVIES, TV, POLITICS, OTHER - GEOGRAPHY, SPORTS, MUSIC + GEOGRAPHY, SPORTS, MUSIC, ART, VIDEOGAMES } diff --git a/questiongenerator/src/main/java/templates/PaintingQuestion.java b/questiongenerator/src/main/java/templates/PaintingQuestion.java new file mode 100644 index 00000000..5281284a --- /dev/null +++ b/questiongenerator/src/main/java/templates/PaintingQuestion.java @@ -0,0 +1,94 @@ +package templates; + +import model.*; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public class PaintingQuestion extends QuestionTemplate { + + List paintingLabels; + + public PaintingQuestion(String langCode) { + super(langCode); + } + + @Override + public void setQuery() { + this.sparqlQuery = + "SELECT DISTINCT ?paintingLabel ?authorLabel ?image " + + "WHERE { " + + " ?painting wdt:P31 wd:Q3305213; " + + " wdt:P170 ?author; " + + " wdt:P18 ?image; " + + " wdt:P1343 wd:Q66362718. " + + " ?author wdt:P106 wd:Q1028181. " + + " SERVICE wikibase:label { bd:serviceParam wikibase:language \"" + langCode + "\". } " + + "} " + + "LIMIT 100"; + } + + @Override + public void processResults() { + paintingLabels = new ArrayList<>(); + List questions = new ArrayList<>(); + List answers = new ArrayList<>(); + + for (int i = 0; i < results.length(); i++) { + + JSONObject result = results.getJSONObject(i); + + + JSONObject paintingLabelObject = result.getJSONObject("paintingLabel"); + String paintingLabel = paintingLabelObject.getString("value"); + + JSONObject authorLabelObject = result.getJSONObject("authorLabel"); + String authorLabel = authorLabelObject.getString("value"); + + JSONObject imageObject = result.getJSONObject("image"); + String imageLink = imageObject.getString("value"); + + if (needToSkip(paintingLabel)) + continue; + + String answerText = ""; + if (langCode.equals("es")) + answerText = paintingLabel + " de " + authorLabel; + else + answerText = paintingLabel + " by " + authorLabel; + + Answer a = new Answer(answerText, AnswerCategory.PAINTING, langCode); + answers.add(a); + + if (langCode.equals("es")) + questions.add(new Question(a, "¿Cuál es este cuadro? " + imageLink, QuestionCategory.ART, QuestionType.IMAGE)); + else + questions.add(new Question(a, "Which painting is this? " + imageLink, QuestionCategory.ART, QuestionType.IMAGE)); + } + + repository.saveAll(new ArrayList<>(answers)); + repository.saveAll(new ArrayList<>(questions)); + } + + private boolean needToSkip(String paintingLabel) { + if (paintingLabels.contains(paintingLabel)) { + return true; + } + paintingLabels.add(paintingLabel); + + boolean isEntityName = true; // Check if it is like Q232334 + if (paintingLabel.startsWith("Q") ){ + for (int i=1; i videoGameLabels; + + public VideogamesPublisherQuestion(String langCode) { + super(langCode); + } + + @Override + public void setQuery() { + this.sparqlQuery = + "SELECT ?gameLabel (MAX(?unitsSoldValue) as ?maxUnitsSold) (SAMPLE(?publisherLabel) as ?publisher) " + + "WHERE { " + + " ?game wdt:P31 wd:Q7889; " + + " wdt:P2664 ?unitsSoldValue. " + + " OPTIONAL { " + + " ?game wdt:P123 ?publisher. " + + " ?publisher rdfs:label ?publisherLabel. " + + " FILTER(LANG(?publisherLabel) IN (\"en\", \"es\")) " + + " } " + + " SERVICE wikibase:label { bd:serviceParam wikibase:language \"" + langCode + "\". } " + + "} " + + "GROUP BY ?game ?gameLabel " + + "ORDER BY DESC(?maxUnitsSold) " + + "LIMIT 150"; + } + + @Override + public void processResults() { + videoGameLabels = new ArrayList<>(); + List questions = new ArrayList<>(); + List answers = new ArrayList<>(); + + for (int i = 0; i < results.length()-10; i++) { + + JSONObject result = results.getJSONObject(i); + + String videoGameLabel = ""; + String publisherLabel = ""; + + try { + JSONObject videoGameLabelObject = result.getJSONObject("gameLabel"); + videoGameLabel = videoGameLabelObject.getString("value"); + + JSONObject publisherLabelObject = result.getJSONObject("publisher"); + publisherLabel = publisherLabelObject.getString("value"); + } catch (Exception e) { + continue; + } + + if (needToSkip(videoGameLabel, publisherLabel)) + continue; + + Answer a = new Answer(publisherLabel, AnswerCategory.GAMES_PUBLISHER, langCode); + answers.add(a); + + if (langCode.equals("es")) + questions.add(new Question(a, "¿Qué compañía publicó " + videoGameLabel + "?", QuestionCategory.VIDEOGAMES, QuestionType.TEXT)); + else + questions.add(new Question(a, "Who published " + videoGameLabel + "?", QuestionCategory.VIDEOGAMES, QuestionType.TEXT)); + } + + repository.saveAll(new ArrayList<>(answers)); + repository.saveAll(new ArrayList<>(questions)); + } + + private boolean needToSkip(String videoGameLabel, String publisherLabel) { + if (videoGameLabels.contains(videoGameLabel)) { + return true; + } + videoGameLabels.add(videoGameLabel); + + boolean isEntityName = isEntityName(videoGameLabel); + if (isEntityName){ + return true; + } + isEntityName = isEntityName(publisherLabel); + if (isEntityName){ + return true; + } + return false; + } + + private boolean isEntityName(String label){ + boolean isEntityName = true; // Check if it is like Q232334 + if (label.startsWith("Q") ){ + for (int i=1; i