diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e698e310..76fff00f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,6 +65,32 @@ jobs: DATABASE_PASSWORD JWT_SECRET SSL_PASSWORD + docker-push-prometheus: + 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/prometheus + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + workdir: api/monitoring/prometheus + docker-push-grafana: + 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/grafana + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + workdir: api/monitoring/grafana docker-push-kiwiq: runs-on: ubuntu-latest needs: [ e2e-tests ] diff --git a/api/monitoring/grafana/Dockerfile b/api/monitoring/grafana/Dockerfile new file mode 100644 index 00000000..624fc546 --- /dev/null +++ b/api/monitoring/grafana/Dockerfile @@ -0,0 +1,14 @@ +FROM grafana/grafana +LABEL authors="dario" +# Define the source and destination directories +COPY_SOURCE = ./provisioning +COPY_DESTINATION = /etc/grafana/provisioning + +# Copy the configuration files +COPY ${COPY_SOURCE}/* ${COPY_DESTINATION} + +# Expose the default Grafana port +EXPOSE 9091 + +# Run Grafana in the foreground +CMD ["grafana-server"] \ No newline at end of file diff --git a/api/monitoring/prometheus/Dockerfile b/api/monitoring/prometheus/Dockerfile new file mode 100644 index 00000000..13f90329 --- /dev/null +++ b/api/monitoring/prometheus/Dockerfile @@ -0,0 +1,14 @@ +FROM prom/prometheus +LABEL authors="dario" +# Define the source and destination directories +COPY_SOURCE = ./configuration +COPY_DESTINATION = /etc/prometheus + +# Copy the configuration files +COPY ${COPY_SOURCE}/* ${COPY_DESTINATION} + +# Expose the default Prometheus port +EXPOSE 9090 + +# Run Prometheus in the foreground +CMD ["prometheus"] diff --git a/api/monitoring/prometheus/prometheus.yml b/api/monitoring/prometheus/configuration/prometheus.yml similarity index 79% rename from api/monitoring/prometheus/prometheus.yml rename to api/monitoring/prometheus/configuration/prometheus.yml index 8b0455fd..8a5f0486 100644 --- a/api/monitoring/prometheus/prometheus.yml +++ b/api/monitoring/prometheus/configuration/prometheus.yml @@ -3,6 +3,6 @@ scrape_configs: metrics_path: '/actuator/prometheus' scrape_interval: 10s static_configs: - - targets: ['host.docker.internal:8080'] + - targets: ['host.docker.internal:8443'] labels: application: 'WIQ API' \ No newline at end of file diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 6678b886..aa965a08 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -58,6 +58,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, Authentication .cors(Customizer.withDefaults()) .sessionManagement(configuration -> configuration.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authorize -> authorize + .requestMatchers(HttpMethod.POST,"/questions/**").permitAll() + .requestMatchers(HttpMethod.GET,"/questions/**").permitAll() + .requestMatchers(HttpMethod.GET,"/users/details").authenticated() + .requestMatchers(HttpMethod.GET,"/users","/users/**").permitAll() .requestMatchers(HttpMethod.GET,"/auth/logout").authenticated() .requestMatchers(HttpMethod.POST,"/auth/**").permitAll() .requestMatchers(HttpMethod.GET, "/swagger/**").permitAll() diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java index 5885412a..895c38fe 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserController.java @@ -1,13 +1,17 @@ package lab.en2b.quizapi.commons.user; +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.commons.user.dtos.UserResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; @RestController @RequestMapping("/users") @@ -23,9 +27,35 @@ public class UserController { * @param authentication the authentication object * @return the response dto for the user details */ + @Operation(summary = "Gets the user details", description = "Gets the user details for the given authentication") + @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("/details") public ResponseEntity getUserDetails(Authentication authentication) { return ResponseEntity.ok(userService.getUserDetailsByAuthentication(authentication)); } + @Operation(summary = "Gets all users", description = "Gets all users") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved") + }) + @GetMapping + public ResponseEntity> getUsers() { + return ResponseEntity.ok(userService.getUsers()); + } + + @Operation(summary = "Gets a user", description = "Gets a user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved") + }) + @Parameters({ + @Parameter(name = "id", description = "The id of the user to get", example = "1") + }) + @GetMapping("/{id}") + public ResponseEntity getUser(@PathVariable Long id) { + return ResponseEntity.ok(userService.getUser(id)); + } + } diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java index 2c4f44cc..949741da 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java @@ -15,8 +15,10 @@ import org.springframework.stereotype.Service; import java.time.Instant; +import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -71,4 +73,12 @@ public User getUserByAuthentication(Authentication authentication) { public UserResponseDto getUserDetailsByAuthentication(Authentication authentication) { return userResponseDtoMapper.apply(getUserByAuthentication(authentication)); } + + public List getUsers() { + return userRepository.findAll().stream().map(userResponseDtoMapper).collect(Collectors.toList()); + } + + public UserResponseDto getUser(Long id) { + return userResponseDtoMapper.apply(userRepository.findById(id).orElseThrow()); + } } 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 index 4e6c2886..eaf16af9 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/utils/GameModeUtils.java @@ -3,6 +3,7 @@ import lab.en2b.quizapi.game.Game; import lab.en2b.quizapi.game.GameMode; import lab.en2b.quizapi.questions.question.QuestionCategory; +import lab.en2b.quizapi.questions.question.dtos.QuestionCategoryDto; import java.util.List; @@ -54,4 +55,71 @@ public static void setGamemodeParams(Game game){ game.setRoundDuration(30); } } + + public static List getQuestionCategories(String lang) { + if(lang == null) + lang = "en"; + if(lang.equals("en")) + return getQuestionCategoriesEn(); + return getQuestionCategoriesEs(); + } + private static List getQuestionCategoriesEn(){ + return List.of( + QuestionCategoryDto.builder() + .name("Art") + .description("Are you an art expert? Prove it!") + .internalRepresentation(QuestionCategory.ART) + .build(), + QuestionCategoryDto.builder() + .name("Music") + .description("Are you a music lover? Prove it!") + .internalRepresentation(QuestionCategory.MUSIC) + .build(), + QuestionCategoryDto.builder() + .name("Geography") + .description("Are you a geography expert? Prove it!") + .internalRepresentation(QuestionCategory.GEOGRAPHY) + .build(), + QuestionCategoryDto.builder() + .name("Sports") + .description("Are you a sports fanatic? Prove it!") + .internalRepresentation(QuestionCategory.SPORTS) + .build(), + QuestionCategoryDto.builder() + .name("Video Games") + .description("Are you a gamer? Prove it!") + .internalRepresentation(QuestionCategory.VIDEOGAMES) + .build() + ); + } + + private static List getQuestionCategoriesEs(){ + return List.of( + QuestionCategoryDto.builder() + .name("Arte") + .description("¿Eres un experto en arte? ¡Demuéstralo!") + .internalRepresentation(QuestionCategory.ART) + .build(), + QuestionCategoryDto.builder() + .name("Música") + .description("¿Eres un melómano? ¡Demuéstralo!") + .internalRepresentation(QuestionCategory.MUSIC) + .build(), + QuestionCategoryDto.builder() + .name("Geografía") + .description("¿Eres un experto en geografía? ¡Demuéstralo!") + .internalRepresentation(QuestionCategory.GEOGRAPHY) + .build(), + QuestionCategoryDto.builder() + .name("Deportes") + .description("¿Eres un fanático de los deportes? ¡Demuéstralo!") + .internalRepresentation(QuestionCategory.SPORTS) + .build(), + QuestionCategoryDto.builder() + .name("Videojuegos") + .description("¿Eres un gamer? ¡Demuéstralo!") + .internalRepresentation(QuestionCategory.VIDEOGAMES) + .build() + ); + } } 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 3192b857..99c1d872 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameController.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameController.java @@ -8,6 +8,7 @@ import jakarta.validation.Valid; import lab.en2b.quizapi.game.dtos.*; import lab.en2b.quizapi.questions.question.QuestionCategory; +import lab.en2b.quizapi.questions.question.dtos.QuestionCategoryDto; import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -133,8 +134,8 @@ public ResponseEntity> getQuestionGameModes(){ @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()); + public ResponseEntity> getQuestionCategories(@RequestParam(required = false) String lang){ + return ResponseEntity.ok(gameService.getQuestionCategories(lang)); } } diff --git a/api/src/main/java/lab/en2b/quizapi/game/GameRepository.java b/api/src/main/java/lab/en2b/quizapi/game/GameRepository.java index 6f747994..13602f47 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameRepository.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameRepository.java @@ -14,6 +14,7 @@ public interface GameRepository extends JpaRepository { @Query(value = "SELECT * FROM Games g WHERE user_id = ?1 AND g.is_game_over = false LIMIT 1", nativeQuery = true) Optional findActiveGameForUser(Long userId); - @Query(value = "COUNT(*) FROM Games g WHERE user_id = ?1 AND g.is_game_over = true", nativeQuery = true) + @Query(value = "SELECT COUNT(*) FROM Games g WHERE user_id = ?1 AND g" + + ".is_game_over = true", nativeQuery = true) Long countFinishedGamesForUser(Long userId); } 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 54eb2a9e..36b9d8a1 100644 --- a/api/src/main/java/lab/en2b/quizapi/game/GameService.java +++ b/api/src/main/java/lab/en2b/quizapi/game/GameService.java @@ -1,10 +1,12 @@ package lab.en2b.quizapi.game; import lab.en2b.quizapi.commons.user.UserService; +import lab.en2b.quizapi.commons.utils.GameModeUtils; 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.QuestionService; +import lab.en2b.quizapi.questions.question.dtos.QuestionCategoryDto; import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; import lab.en2b.quizapi.statistics.Statistics; @@ -151,8 +153,8 @@ public GameResponseDto getGameDetails(Long id, Authentication authentication) { return gameResponseDtoMapper.apply(game); } - public List getQuestionCategories() { - return Arrays.asList(QuestionCategory.values()); + public List getQuestionCategories(String lang) { + return GameModeUtils.getQuestionCategories(lang); } private boolean wasGameMeantToBeOver(Game game) { diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java index e81ca5ac..b726e14d 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/QuestionController.java @@ -1,6 +1,7 @@ package lab.en2b.quizapi.questions.question; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; @@ -12,6 +13,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequestMapping("/questions") @RequiredArgsConstructor @@ -21,7 +24,6 @@ public class QuestionController { @Operation(summary = "Sends an answer", description = "Sends the answer dto for a given question id") @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), @ApiResponse(responseCode = "404", description = "Not found - There is not a question with that id", content = @io.swagger.v3.oas.annotations.media.Content) }) @PostMapping("/{questionId}/answer") @@ -32,10 +34,9 @@ private ResponseEntity answerQuestion(@PathVariable @Pos @Operation(summary = "Gets a random question", description = "Gets a random question in the language") @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), @ApiResponse(responseCode = "404", description = "Language does not exist or it is misspelled", content = @io.swagger.v3.oas.annotations.media.Content) }) - @GetMapping("/new") + @GetMapping("/random") private ResponseEntity generateQuestion(@RequestParam(required = false) String lang){ return ResponseEntity.ok(questionService.getRandomQuestion(lang)); } @@ -43,11 +44,22 @@ private ResponseEntity generateQuestion(@RequestParam(requi @Operation(summary = "Gets a question", description = "Gets a question given a question id") @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), @ApiResponse(responseCode = "404", description = "Not found - There is not a question with that id", content = @io.swagger.v3.oas.annotations.media.Content) }) @GetMapping("/{id}") private ResponseEntity getQuestionById(@PathVariable @PositiveOrZero Long id){ return ResponseEntity.ok(questionService.getQuestionById(id)); } + + @Operation(summary = "Gets a list of questions", description = "Gets a list of questions given a page number") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved"), + @ApiResponse(responseCode = "404", description = "Not found - There are no questions", content = @io.swagger.v3.oas.annotations.media.Content), + @ApiResponse(responseCode = "400", description = "Bad request - The page number is invalid", content = @io.swagger.v3.oas.annotations.media.Content) + }) + @Parameter(name = "page", description = "The page number. Cannot be lower or equal to 0.", required = true) + @GetMapping + private ResponseEntity> getQuestions(@RequestParam Long page){ + return ResponseEntity.ok(questionService.getQuestionsWithPage(page)); + } } 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 e0c420e4..093a1368 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 @@ -69,6 +69,33 @@ public QuestionResponseDto getQuestionById(Long id) { return questionResponseDtoMapper.apply(q); } + /** + * Get a list of questions with a page + * @param page The page number + * @return the list of questions + */ + public List getQuestionsWithPage(Long page){ + if (page < 1) + throw new IllegalArgumentException("Invalid page number"); + List result = questionRepository.findAll().stream() + .map(questionResponseDtoMapper).toList(); + return getPage(result, page); + } + + private List getPage(List result, Long page) { + try{ + int QUESTION_PAGE_SIZE = 100; + int startIndex = Math.toIntExact((page-1)* QUESTION_PAGE_SIZE); + if(startIndex > result.size()) + throw new IllegalArgumentException("Invalid page number, maximum page is "+(result.size()/ QUESTION_PAGE_SIZE +1) + " and you requested page "+page); + if (result.size() < page* QUESTION_PAGE_SIZE) + return result.subList(startIndex,result.size()); + return result.subList(startIndex, Math.toIntExact(page* QUESTION_PAGE_SIZE)); + } catch (ArithmeticException e) { + throw new IllegalArgumentException("Invalid page number"); + } + + } /** * Load the answers for a question (The distractors and the correct one) diff --git a/api/src/main/java/lab/en2b/quizapi/questions/question/dtos/QuestionCategoryDto.java b/api/src/main/java/lab/en2b/quizapi/questions/question/dtos/QuestionCategoryDto.java new file mode 100644 index 00000000..3d9180b4 --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/questions/question/dtos/QuestionCategoryDto.java @@ -0,0 +1,21 @@ +package lab.en2b.quizapi.questions.question.dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lab.en2b.quizapi.questions.question.QuestionCategory; +import lombok.*; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Setter +public class QuestionCategoryDto { + @Schema(description = "Beautified name of the question category",example = "Sports") + private String name; + @Schema(description = "Description of the question category",example = "Test description of the question category") + private String description; + @JsonProperty("internal_representation") + @Schema(description = "Internal code used for describing the question category",example = "SPORTS") + private QuestionCategory internalRepresentation; +} 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 5063486b..ef3d483a 100644 --- a/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/game/GameServiceTest.java @@ -468,7 +468,7 @@ public void getGameDetailsInvalidId(){ @Test public void testGetQuestionCategories(){ - assertEquals(Arrays.asList(QuestionCategory.values()), gameService.getQuestionCategories()); + assertEquals(QuestionCategory.values().length, gameService.getQuestionCategories(null).size()); } @Test diff --git a/api/src/test/java/lab/en2b/quizapi/questions/QuestionControllerTest.java b/api/src/test/java/lab/en2b/quizapi/questions/QuestionControllerTest.java index 4bf7be42..ead2efc9 100644 --- a/api/src/test/java/lab/en2b/quizapi/questions/QuestionControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/questions/QuestionControllerTest.java @@ -35,16 +35,16 @@ public class QuestionControllerTest { UserService userService; @Test - void newQuestionShouldReturn403() throws Exception{ - mockMvc.perform(get("/questions/new?lang=en") + void randomQuestionNoAuthShouldReturn200() throws Exception{ + mockMvc.perform(get("/questions/random?lang=en") .contentType("application/json") .with(csrf())) - .andExpect(status().isForbidden()); + .andExpect(status().isOk()); } @Test - void newQuestionShouldReturn200() throws Exception{ - mockMvc.perform(get("/questions/new?lang=en") + void randomQuestionShouldReturn200() throws Exception{ + mockMvc.perform(get("/questions/random?lang=en") .with(user("test").roles("user")) .contentType("application/json") .with(csrf())) @@ -52,8 +52,8 @@ void newQuestionShouldReturn200() throws Exception{ } @Test - void newQuestionNoLangShouldReturn200() throws Exception{ - mockMvc.perform(get("/questions/new") + void randomQuestionNoLangShouldReturn200() throws Exception{ + mockMvc.perform(get("/questions/random") .with(user("test").roles("user")) .contentType("application/json") .with(csrf())) @@ -61,11 +61,11 @@ void newQuestionNoLangShouldReturn200() throws Exception{ } @Test - void questionByIdShouldReturn403() throws Exception{ + void questionByIdNoAuthShouldReturn200() throws Exception{ mockMvc.perform(get("/questions/1") .contentType("application/json") .with(csrf())) - .andExpect(status().isForbidden()); + .andExpect(status().isOk()); } @Test @@ -86,11 +86,12 @@ void questionNegativeIdShouldReturn400() throws Exception{ } @Test - void answerQuestionShouldReturn403() throws Exception{ - mockMvc.perform(get("/questions/1/answer") + void answerQuestionNoAuthShouldReturn200() throws Exception{ + mockMvc.perform(post("/questions/1/answer") + .content(asJsonString(new AnswerDto(1L))) .contentType("application/json") .with(csrf())) - .andExpect(status().isForbidden()); + .andExpect(status().isOk()); } @Test @@ -137,4 +138,30 @@ void answerQuestionNegativeIdShouldReturn400() throws Exception{ .with(csrf())) .andExpect(status().isBadRequest()); } + + @Test + void getQuestionsWithPageNoAuthShouldReturn200() throws Exception{ + mockMvc.perform(get("/questions?page=1") + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void getQuestionsWithPageShouldReturn200() throws Exception{ + mockMvc.perform(get("/questions?page=1") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void getQuestionsWithPageNoPageShouldReturn400() throws Exception{ + mockMvc.perform(get("/questions") + .with(user("test").roles("user")) + .contentType("application/json") + .with(csrf())) + .andExpect(status().isBadRequest()); + } } 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 3f30fc88..a773740c 100644 --- a/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/questions/QuestionServiceTest.java @@ -10,6 +10,7 @@ import lab.en2b.quizapi.questions.question.dtos.AnswerCheckResponseDto; import lab.en2b.quizapi.questions.question.dtos.QuestionResponseDto; import lab.en2b.quizapi.questions.question.mappers.QuestionResponseDtoMapper; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -175,5 +176,28 @@ void testAnswerQuestionInvalidAnswer(){ assertThrows(IllegalArgumentException.class,() -> questionService.answerQuestion(1L, AnswerDto.builder().answerId(3L).build())); } + @Test + void getQuestionsWithPage() { + when(questionRepository.findAll()).thenReturn(List.of(defaultQuestion)); + List response = questionService.getQuestionsWithPage(1L); + assertEquals(response, List.of(defaultResponseDto)); + } + + @Test + void getQuestionsWithPageInvalidPage() { + assertThrows(IllegalArgumentException.class,() -> questionService.getQuestionsWithPage(0L)); + } + @Test + void getQuestionsWithPageGreaterThanSize() { + when(questionRepository.findAll()).thenReturn(List.of(defaultQuestion)); + + assertThrows(IllegalArgumentException.class,() -> questionService.getQuestionsWithPage(2L)); + } + + @Test + void getQuestionsWithPageNoQuestions() { + when(questionRepository.findAll()).thenReturn(List.of()); + Assertions.assertEquals(questionService.getQuestionsWithPage(1L), List.of()); + } } diff --git a/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java b/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java index 4d090f5e..bf6df6df 100644 --- a/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java +++ b/api/src/test/java/lab/en2b/quizapi/user/UserControllerTest.java @@ -31,16 +31,42 @@ public class UserControllerTest { UserService userService; @Test - void getUserShouldReturn200() throws Exception{ + void getPersonalDetailsShouldReturn200() throws Exception{ mockMvc.perform(get("/users/details") .with(user("test").roles("user"))) .andExpect(status().isOk()); } @Test - void getUserShouldReturn403() throws Exception{ + void getPersonalDetailsShouldReturn403() throws Exception{ mockMvc.perform(get("/users/details")) .andExpect(status().isForbidden()); } + @Test + void getUsersShouldReturn200() throws Exception{ + mockMvc.perform(get("/users") + .with(user("test").roles("user"))) + .andExpect(status().isOk()); + } + + @Test + void getUsersNoAuthShouldReturn200() throws Exception{ + mockMvc.perform(get("/users")) + .andExpect(status().isOk()); + } + + @Test + void getUserShouldReturn200() throws Exception{ + mockMvc.perform(get("/users/1") + .with(user("test").roles("user"))) + .andExpect(status().isOk()); + } + + @Test + void getUserNoAuthShouldReturn200() throws Exception{ + mockMvc.perform(get("/users/1")) + .andExpect(status().isOk()); + } + } diff --git a/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java b/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java index b99981af..ed698280 100644 --- a/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java +++ b/api/src/test/java/lab/en2b/quizapi/user/UserServiceTest.java @@ -17,6 +17,8 @@ import org.springframework.security.core.Authentication; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.util.ArrayList; +import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; @@ -33,13 +35,16 @@ public class UserServiceTest { @Mock private UserRepository userRepository; + private UserResponseDtoMapper userResponseDtoMapper; + private User defaultUser; private UserResponseDto defaultUserResponseDto; @BeforeEach public void setUp() { - userService = new UserService(userRepository, new UserResponseDtoMapper()); + this.userResponseDtoMapper = new UserResponseDtoMapper(); + userService = new UserService(userRepository, userResponseDtoMapper); defaultUser = User.builder() .id(1L) .username("HordyJurtado") @@ -55,7 +60,7 @@ public void setUp() { } @Test - public void getUserDetailsTest(){ + public void getPersonalDetailsTest(){ Authentication authentication = mock(Authentication.class); when(authentication.getPrincipal()).thenReturn(UserDetailsImpl.build(defaultUser)); when(userRepository.findByEmail(any())).thenReturn(Optional.of(defaultUser)); @@ -64,11 +69,58 @@ public void getUserDetailsTest(){ } @Test - public void getUserDetailsWhenNotFound() { + public void getPersonalDetailsWhenNotFound() { Authentication authentication = mock(Authentication.class); when(authentication.getPrincipal()).thenReturn(UserDetailsImpl.build(defaultUser)); when(userRepository.findByEmail(any())).thenReturn(Optional.empty()); Assertions.assertThrows(NoSuchElementException.class, () -> userService.getUserDetailsByAuthentication(authentication)); } + @Test + public void getUsersTestWhenThereAreMultiplePeople(){ + User defaultUser1 = User.builder() + .id(1L) + .username("HordyJurtado") + .email("test1@test1.com") + .password("password") + .role("ROLE_USER") + .build(); + User defaultUser2 = User.builder() + .id(2L) + .username("HordyJurtado") + .email("test2@test2.com") + .password("password") + .role("ROLE_USER") + .build(); + User defaultUser3 = User.builder() + .id(3L) + .username("HordyJurtado") + .email("test3@test3.com") + .password("password") + .role("ROLE_USER") + .build(); + List userResult = List.of(defaultUser1, defaultUser2, defaultUser3); + when(userRepository.findAll()).thenReturn(userResult); + List result = userResult.stream().map(userResponseDtoMapper::apply).toList(); + Assertions.assertEquals(result, userService.getUsers()); + } + + @Test + public void getUsersTestWhenThereIsNoPeople(){ + when(userRepository.findAll()).thenReturn(new ArrayList<>()); + Assertions.assertEquals(List.of(), userService.getUsers()); + } + + @Test + public void getUserTest(){ + when(userRepository.findById(1L)).thenReturn(Optional.of(defaultUser)); + Assertions.assertEquals(defaultUserResponseDto, userService.getUser(1L)); + } + + @Test + public void getUserTestWhenNotFound(){ + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + Assertions.assertThrows(NoSuchElementException.class, () -> userService.getUser(1L)); + } + } diff --git a/docker-compose.yml b/docker-compose.yml index 8fa734aa..e587ec0a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,19 +57,6 @@ depends_on: - WIQ_DB - 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 @@ -99,16 +86,32 @@ - REACT_APP_API_ENDPOINT=${API_URI} networks: mynetwork: + + prometheus: + image: ghcr.io/arquisoft/wiq_en2b/prometheus:latest + container_name: prometheus-${teamname:-defaultASW} + profiles: ["dev", "prod"] + ports: + - "9090:9090" + networks: + mynetwork: + volumes: + - prometheus_data:/prometheus + - /certs:/etc/letsencrypt/kiwiq.run.place:ro + depends_on: + - api grafana: - image: grafana/grafana + image: ghcr.io/arquisoft/wiq_en2b/grafana:latest container_name: grafana-${teamname:-defaultASW} profiles: [ "dev" , "prod"] networks: mynetwork: volumes: - grafana_data:/var/lib/grafana - - ./quiz-api/monitoring/grafana/provisioning:/etc/grafana/provisioning + - /certs:/etc/letsencrypt/kiwiq.run.place:ro + ports: + - "9091:9091" environment: - GF_SERVER_HTTP_PORT=9091 - GF_AUTH_DISABLE_LOGIN_FORM=true