From 8c2e7e51832ef60a0493c73b402f6c349375910d Mon Sep 17 00:00:00 2001
From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com>
Date: Thu, 11 Dec 2025 20:04:19 +0200
Subject: [PATCH 01/41] configured actuator (#8)
---
pom.xml | 5 ++++-
src/main/resources/application.properties | 4 ++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index c8f9aa8..7b5288f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -80,7 +80,10 @@
h2
test
-
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index e400403..34f5576 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -13,3 +13,7 @@ spring.jpa.hibernate.ddl-auto=none
server.servlet.context-path=/api
rawg.key=${RAWG_KEY}
+
+management.endpoints.web.exposure.include=health
+management.endpoint.health.probes.enabled=true
+management.endpoint.health.show-details=always
From f4d43743d6c2a09086f4cc9db9aa078aa0cfedd0 Mon Sep 17 00:00:00 2001
From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com>
Date: Sat, 13 Dec 2025 12:45:57 +0200
Subject: [PATCH 02/41] Improve filtering (#9)
* implemented search games method
* refactored getById to use apiId
* ran mvnclean package
---
pom.xml | 4 +
.../backend/controller/GameController.java | 82 +++++++++----------
.../dto/internal/GameSearchParameters.java | 11 +++
...pecificationProviderNotFoundException.java | 7 ++
.../backend/model/Game.java | 14 ++++
.../backend/model/Genre.java | 1 +
.../backend/model/Platform.java | 1 +
.../backend/repository/GameRepository.java | 14 +---
.../repository/SpecificationBuilder.java | 7 ++
.../repository/SpecificationProvider.java | 9 ++
.../SpecificationProviderManager.java | 5 ++
.../game/GameSpecificationBuilder.java | 60 ++++++++++++++
.../GameSpecificationProviderManager.java | 28 +++++++
.../spec/GameGenresSpecificationProvider.java | 47 +++++++++++
.../spec/GameNameSpecificationProvider.java | 37 +++++++++
.../GamePlatformsSpecificationProvider.java | 48 +++++++++++
.../spec/GameYearSpecificationProvider.java | 35 ++++++++
.../backend/service/GameService.java | 11 +--
.../backend/service/GameServiceImpl.java | 29 +++----
src/main/resources/application.properties | 4 +
20 files changed, 372 insertions(+), 82 deletions(-)
create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/exception/SpecificationProviderNotFoundException.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/SpecificationBuilder.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/SpecificationProvider.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/SpecificationProviderManager.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationProviderManager.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameGenresSpecificationProvider.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameNameSpecificationProvider.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java
diff --git a/pom.xml b/pom.xml
index 7b5288f..42c589c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,6 +54,10 @@
org.springframework.boot
spring-boot-starter-jdbc
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
mysql
diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java
index ba1d65b..1ca3d37 100644
--- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java
+++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java
@@ -1,17 +1,19 @@
package com.videogamescatalogue.backend.controller;
import com.videogamescatalogue.backend.dto.internal.GameDto;
+import com.videogamescatalogue.backend.dto.internal.GameSearchParameters;
import com.videogamescatalogue.backend.model.Genre;
import com.videogamescatalogue.backend.model.Platform;
import com.videogamescatalogue.backend.service.GameService;
import java.util.Arrays;
+import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
-import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -20,11 +22,12 @@
@RequestMapping("/games")
@RestController
public class GameController {
+ public static final int DEFAULT_PAGE_SIZE = 30;
private final GameService gameService;
@GetMapping("/local")
public Page getAllGamesFromDb(
- @PageableDefault(size = 30, sort = "apiRating",
+ @PageableDefault(size = DEFAULT_PAGE_SIZE, sort = "apiRating",
direction = Sort.Direction.DESC)
Pageable pageable
) {
@@ -32,55 +35,44 @@ public Page getAllGamesFromDb(
}
@GetMapping("/local/id/{id}")
- public GameDto getFromDbById(@PathVariable Long id) {
- return gameService.getFromDbById(id);
+ public GameDto getFromDbByApiId(@PathVariable Long id) {
+ return gameService.getFromDbByApiId(id);
}
- @GetMapping("/local/genre/{genre}")
- public ResponseEntity> getFromDbByGenre(
- @PathVariable String genre,
- @PageableDefault(size = 30)
+ @GetMapping("/local/search")
+ public Page search(
+ @ModelAttribute GameSearchParameters searchParameters,
+ @PageableDefault(size = DEFAULT_PAGE_SIZE)
Pageable pageable
) {
- Genre.Name name;
- try {
- name = Genre.Name.valueOf(genre.toUpperCase());
- } catch (IllegalArgumentException ex) {
- return ResponseEntity
- .badRequest()
- .body("There is no genre by name: " + genre
- + ". Allowed values: " + Arrays.asList(Genre.Name.values())
- );
- }
- return ResponseEntity.ok(gameService.getFromDbByGenre(name, pageable));
- }
-
- @GetMapping("/local/year/{year}")
- public Page getFromDbByYear(
- @PathVariable int year,
- @PageableDefault(size = 30)
- Pageable pageable
- ) {
- return gameService.getByYear(year, pageable);
+ validateSearchParams(searchParameters);
+ return gameService.search(searchParameters, pageable);
}
- @GetMapping("/local/platform/{platform}")
- public ResponseEntity> getFromDbByPlatform(
- @PathVariable String platform,
- @PageableDefault(size = 30)
- Pageable pageable
- ) {
- Platform.GeneralName name;
- try {
- name = Platform.GeneralName.valueOf(platform.toUpperCase());
-
- } catch (IllegalArgumentException ex) {
- return ResponseEntity
- .badRequest()
- .body("There is no platform by name: " + platform
- + ". Allowed values: " + Arrays.asList(Platform.GeneralName.values())
- );
+ private void validateSearchParams(GameSearchParameters searchParameters) {
+ if (searchParameters.platforms() != null) {
+ try {
+ searchParameters.platforms()
+ .forEach(
+ p -> Platform.GeneralName.valueOf(p.toUpperCase())
+ );
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ "Invalid platforms provided. Valid platforms: "
+ + Arrays.stream(Platform.GeneralName.values())
+ .map(Enum::toString)
+ .collect(Collectors.joining(", ")), e);
+ }
+ }
+ if (searchParameters.genres() != null) {
+ try {
+ searchParameters.genres().forEach(g -> Genre.Name.valueOf(g.toUpperCase()));
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid genres provided. Valid genres: "
+ + Arrays.stream(Genre.Name.values())
+ .map(Enum::toString)
+ .collect(Collectors.joining(", ")), e);
+ }
}
- return ResponseEntity.ok(gameService.getFromDbByPlatform(name, pageable));
}
}
diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java
new file mode 100644
index 0000000..ad56359
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java
@@ -0,0 +1,11 @@
+package com.videogamescatalogue.backend.dto.internal;
+
+import java.util.List;
+
+public record GameSearchParameters(
+ String name,
+ Integer year,
+ List platforms,
+ List genres
+) {
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/exception/SpecificationProviderNotFoundException.java b/src/main/java/com/videogamescatalogue/backend/exception/SpecificationProviderNotFoundException.java
new file mode 100644
index 0000000..06b67c4
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/exception/SpecificationProviderNotFoundException.java
@@ -0,0 +1,7 @@
+package com.videogamescatalogue.backend.exception;
+
+public class SpecificationProviderNotFoundException extends RuntimeException {
+ public SpecificationProviderNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java
index 41f8ab0..00f762c 100644
--- a/src/main/java/com/videogamescatalogue/backend/model/Game.java
+++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java
@@ -60,4 +60,18 @@ public class Game {
@Column(nullable = false, name = "is_deleted")
private boolean isDeleted = false;
+
+ @Getter
+ public enum SpecificationKey {
+ NAME("name"),
+ YEAR("year"),
+ PLATFORMS("platforms"),
+ GENRES("genres");
+
+ private final String value;
+
+ SpecificationKey(String value) {
+ this.value = value;
+ }
+ }
}
diff --git a/src/main/java/com/videogamescatalogue/backend/model/Genre.java b/src/main/java/com/videogamescatalogue/backend/model/Genre.java
index a7fddd9..094735f 100644
--- a/src/main/java/com/videogamescatalogue/backend/model/Genre.java
+++ b/src/main/java/com/videogamescatalogue/backend/model/Genre.java
@@ -31,6 +31,7 @@ public class Genre {
@Column(nullable = false, name = "is_deleted")
private boolean isDeleted = false;
+ @Getter
public enum Name {
ACTION("Action"),
INDIE("Indie"),
diff --git a/src/main/java/com/videogamescatalogue/backend/model/Platform.java b/src/main/java/com/videogamescatalogue/backend/model/Platform.java
index 7c4a3bd..7c5120d 100644
--- a/src/main/java/com/videogamescatalogue/backend/model/Platform.java
+++ b/src/main/java/com/videogamescatalogue/backend/model/Platform.java
@@ -31,6 +31,7 @@ public class Platform {
@Column(nullable = false, name = "is_deleted")
private boolean isDeleted = false;
+ @Getter
public enum GeneralName {
PC("PC"),
PLAYSTATION("PlayStation"),
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java
index 635aa16..eda9e08 100644
--- a/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java
+++ b/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java
@@ -1,16 +1,10 @@
package com.videogamescatalogue.backend.repository;
import com.videogamescatalogue.backend.model.Game;
-import com.videogamescatalogue.backend.model.Genre;
-import com.videogamescatalogue.backend.model.Platform;
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.Pageable;
+import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
-public interface GameRepository extends JpaRepository {
- Page findByGenresName(Genre.Name genre, Pageable pageable);
-
- Page findByYear(int year, Pageable pageable);
-
- Page findByPlatformsGeneralName(Platform.GeneralName generalName, Pageable pageable);
+public interface GameRepository extends JpaRepository, JpaSpecificationExecutor {
+ Optional findByApiId(Long apiId);
}
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/SpecificationBuilder.java b/src/main/java/com/videogamescatalogue/backend/repository/SpecificationBuilder.java
new file mode 100644
index 0000000..460beb7
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/repository/SpecificationBuilder.java
@@ -0,0 +1,7 @@
+package com.videogamescatalogue.backend.repository;
+
+import org.springframework.data.jpa.domain.Specification;
+
+public interface SpecificationBuilder {
+ Specification build(T searchParameters);
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/SpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/SpecificationProvider.java
new file mode 100644
index 0000000..7873128
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/repository/SpecificationProvider.java
@@ -0,0 +1,9 @@
+package com.videogamescatalogue.backend.repository;
+
+import org.springframework.data.jpa.domain.Specification;
+
+public interface SpecificationProvider {
+ String getKey();
+
+ Specification getSpecification(P param);
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/SpecificationProviderManager.java b/src/main/java/com/videogamescatalogue/backend/repository/SpecificationProviderManager.java
new file mode 100644
index 0000000..d0a55bf
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/repository/SpecificationProviderManager.java
@@ -0,0 +1,5 @@
+package com.videogamescatalogue.backend.repository;
+
+public interface SpecificationProviderManager {
+ SpecificationProvider getSpecificationProvider(String key);
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java
new file mode 100644
index 0000000..ad5ee03
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java
@@ -0,0 +1,60 @@
+package com.videogamescatalogue.backend.repository.game;
+
+import com.videogamescatalogue.backend.dto.internal.GameSearchParameters;
+import com.videogamescatalogue.backend.model.Game;
+import com.videogamescatalogue.backend.repository.SpecificationBuilder;
+import com.videogamescatalogue.backend.repository.SpecificationProvider;
+import com.videogamescatalogue.backend.repository.SpecificationProviderManager;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class GameSpecificationBuilder
+ implements SpecificationBuilder {
+ private final SpecificationProviderManager specificationProviderManager;
+
+ @Override
+ public Specification build(GameSearchParameters searchParameters) {
+ if (searchParameters == null) {
+ throw new IllegalArgumentException("Search Parameters cannot be null");
+ }
+ Specification specification = Specification.where(null);
+ specification = getSpecificationForParam(
+ searchParameters.name(),
+ Game.SpecificationKey.NAME.getValue(), specification
+ );
+ specification = getSpecificationForParam(
+ searchParameters.year(),
+ Game.SpecificationKey.YEAR.getValue(), specification
+ );
+ specification = getSpecificationForParam(
+ searchParameters.platforms(),
+ Game.SpecificationKey.PLATFORMS.getValue(), specification
+ );
+ specification = getSpecificationForParam(
+ searchParameters.genres(),
+ Game.SpecificationKey.GENRES.getValue(), specification
+ );
+
+ return specification;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Specification getSpecificationForParam(
+ T searchParameter,
+ String key,
+ Specification specification
+ ) {
+ if (searchParameter != null) {
+ SpecificationProvider provider =
+ (SpecificationProvider) specificationProviderManager
+ .getSpecificationProvider(key);
+ return specification.and(
+ provider.getSpecification(searchParameter)
+ );
+ }
+ return specification;
+ }
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationProviderManager.java b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationProviderManager.java
new file mode 100644
index 0000000..ea06f5b
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationProviderManager.java
@@ -0,0 +1,28 @@
+package com.videogamescatalogue.backend.repository.game;
+
+import com.videogamescatalogue.backend.exception.SpecificationProviderNotFoundException;
+import com.videogamescatalogue.backend.model.Game;
+import com.videogamescatalogue.backend.repository.SpecificationProvider;
+import com.videogamescatalogue.backend.repository.SpecificationProviderManager;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@RequiredArgsConstructor
+@Component
+public class GameSpecificationProviderManager
+ implements SpecificationProviderManager {
+ private final List> specificationProvidersList;
+
+ @Override
+ public SpecificationProvider getSpecificationProvider(String key) {
+ return specificationProvidersList.stream()
+ .filter(provider -> provider.getKey().equals(key))
+ .findFirst()
+ .orElseThrow(
+ () -> new SpecificationProviderNotFoundException(
+ "Can't find SpecificationProvider for key: "
+ + key)
+ );
+ }
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameGenresSpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameGenresSpecificationProvider.java
new file mode 100644
index 0000000..e17716f
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameGenresSpecificationProvider.java
@@ -0,0 +1,47 @@
+package com.videogamescatalogue.backend.repository.game.spec;
+
+import com.videogamescatalogue.backend.model.Game;
+import com.videogamescatalogue.backend.model.Genre;
+import com.videogamescatalogue.backend.repository.SpecificationProvider;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Join;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+import java.util.List;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Component;
+
+@Component
+public class GameGenresSpecificationProvider implements SpecificationProvider> {
+
+ public static final String KEY = Game.SpecificationKey.GENRES.getValue();
+
+ @Override
+ public String getKey() {
+ return KEY;
+ }
+
+ @Override
+ public Specification getSpecification(List param) {
+ return new Specification() {
+ @Override
+ public Predicate toPredicate(
+ Root root,
+ CriteriaQuery> query,
+ CriteriaBuilder criteriaBuilder
+ ) {
+ query.distinct(true);
+
+ List names = param.stream()
+ .map(String::toUpperCase)
+ .map(Genre.Name::valueOf)
+ .toList();
+
+ Join genreJoin = root.join(KEY);
+
+ return genreJoin.get("name").in(names);
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameNameSpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameNameSpecificationProvider.java
new file mode 100644
index 0000000..cb7f2a0
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameNameSpecificationProvider.java
@@ -0,0 +1,37 @@
+package com.videogamescatalogue.backend.repository.game.spec;
+
+import com.videogamescatalogue.backend.model.Game;
+import com.videogamescatalogue.backend.repository.SpecificationProvider;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Component;
+
+@Component
+public class GameNameSpecificationProvider implements SpecificationProvider {
+
+ public static final String KEY = Game.SpecificationKey.NAME.getValue();
+
+ @Override
+ public String getKey() {
+ return KEY;
+ }
+
+ @Override
+ public Specification getSpecification(String param) {
+ return new Specification() {
+ @Override
+ public Predicate toPredicate(
+ Root root,
+ CriteriaQuery> query,
+ CriteriaBuilder criteriaBuilder
+ ) {
+ return criteriaBuilder.like(
+ criteriaBuilder.lower(root.get(KEY)),
+ "%" + param.toLowerCase() + "%");
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java
new file mode 100644
index 0000000..c72ceae
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java
@@ -0,0 +1,48 @@
+package com.videogamescatalogue.backend.repository.game.spec;
+
+import com.videogamescatalogue.backend.model.Game;
+import com.videogamescatalogue.backend.model.Platform;
+import com.videogamescatalogue.backend.repository.SpecificationProvider;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Join;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+import java.util.List;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Component;
+
+@Component
+public class GamePlatformsSpecificationProvider
+ implements SpecificationProvider> {
+
+ public static final String KEY = Game.SpecificationKey.PLATFORMS.getValue();
+
+ @Override
+ public String getKey() {
+ return KEY;
+ }
+
+ @Override
+ public Specification getSpecification(List param) {
+ return new Specification() {
+ @Override
+ public Predicate toPredicate(
+ Root root,
+ CriteriaQuery> query,
+ CriteriaBuilder criteriaBuilder
+ ) {
+ query.distinct(true);
+
+ List names = param.stream()
+ .map(String::toUpperCase)
+ .map(Platform.GeneralName::valueOf)
+ .toList();
+
+ Join
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.2.0
+
diff --git a/src/main/java/com/videogamescatalogue/backend/config/OpenApiConfig.java b/src/main/java/com/videogamescatalogue/backend/config/OpenApiConfig.java
new file mode 100644
index 0000000..ee8f154
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/config/OpenApiConfig.java
@@ -0,0 +1,26 @@
+package com.videogamescatalogue.backend.config;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class OpenApiConfig {
+ public static final String SECURITY_SCHEME_NAME = "BearerAuth";
+ public static final String BEARER_FORMAT = "JWT";
+ public static final String SCHEME_TYPE = "bearer";
+
+ @Bean
+ public OpenAPI customOpenApi() {
+ return new OpenAPI()
+ .components(new Components().addSecuritySchemes(SECURITY_SCHEME_NAME,
+ new SecurityScheme()
+ .type(SecurityScheme.Type.HTTP)
+ .scheme(SCHEME_TYPE)
+ .bearerFormat(BEARER_FORMAT)))
+ .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME));
+ }
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java b/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java
index e4fd185..f4f7c3f 100644
--- a/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java
+++ b/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java
@@ -6,6 +6,11 @@
import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto;
import com.videogamescatalogue.backend.security.AuthenticationService;
import com.videogamescatalogue.backend.service.user.UserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
@@ -13,6 +18,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+@Tag(name = "Authentication", description = "Register and login users")
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@@ -20,6 +26,24 @@ public class AuthenticationController {
private final UserService userService;
private final AuthenticationService authenticationService;
+ @Operation(
+ summary = "Register a new user",
+ description = "Creates a new user account and returns basic user information",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "User registered successfully",
+ content = @Content(schema = @Schema(
+ implementation = UserResponseDto.class
+ ))
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Validation error",
+ content = @Content
+ )
+ }
+ )
@PostMapping("/registration")
UserResponseDto registerUser(
@RequestBody @Valid UserRegistrationRequestDto requestDto
@@ -27,6 +51,24 @@ UserResponseDto registerUser(
return userService.registerUser(requestDto);
}
+ @Operation(
+ summary = "User login",
+ description = "Authenticates a user and returns a JWT token",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Authentication successful",
+ content = @Content(schema = @Schema(
+ implementation = UserLoginResponseDto.class
+ ))
+ ),
+ @ApiResponse(
+ responseCode = "401",
+ description = "Invalid credentials",
+ content = @Content
+ )
+ }
+ )
@PostMapping("/login")
UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto requestDto) {
return authenticationService.authenticate(requestDto);
diff --git a/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java b/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java
index 9713360..f26275c 100644
--- a/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java
+++ b/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java
@@ -5,6 +5,11 @@
import com.videogamescatalogue.backend.dto.internal.comment.UpdateCommentRequestDto;
import com.videogamescatalogue.backend.model.User;
import com.videogamescatalogue.backend.service.comment.CommentService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
@@ -22,6 +27,7 @@
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
+@Tag(name = "Comments", description = "Create, update, delete, and view comments for games")
@RequiredArgsConstructor
@RequestMapping
@RestController
@@ -29,6 +35,23 @@ public class CommentController {
private static final int DEFAULT_PAGE_SIZE = 30;
private final CommentService commentService;
+ @Operation(
+ summary = "Create a comment for a game",
+ description = """
+ Creates a new comment for the specified game.
+ User must be authenticated.
+ """
+ )
+ @ApiResponse(
+ responseCode = "201",
+ description = "Comment created successfully",
+ content = @Content(schema = @Schema(implementation = CommentDto.class))
+ )
+ @ApiResponse(
+ responseCode = "401",
+ description = "User is not authenticated",
+ content = @Content
+ )
@PostMapping("/comments/games/{gameApiId}")
@ResponseStatus(value = HttpStatus.CREATED)
public CommentDto create(
@@ -39,6 +62,15 @@ public CommentDto create(
return commentService.create(gameApiId, requestDto, user);
}
+ @Operation(
+ summary = "Get comments for a game",
+ description = "Returns paginated comments for a specific game"
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Comments retrieved successfully",
+ content = @Content(schema = @Schema(implementation = CommentDto.class))
+ )
@GetMapping("/games/{gameApiId}/comments")
public Page getCommentsForGame(
@PathVariable Long gameApiId,
@@ -48,6 +80,20 @@ public Page getCommentsForGame(
return commentService.getCommentsForGame(gameApiId, pageable);
}
+ @Operation(
+ summary = "Get authenticated user's comments",
+ description = "Returns paginated comments created by the authenticated user"
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "User comments retrieved successfully",
+ content = @Content(schema = @Schema(implementation = CommentDto.class))
+ )
+ @ApiResponse(
+ responseCode = "401",
+ description = "User is not authenticated",
+ content = @Content
+ )
@GetMapping("/comments")
public Page getUserComments(
@AuthenticationPrincipal User user,
@@ -57,6 +103,20 @@ public Page getUserComments(
return commentService.getUserComments(user.getId(), pageable);
}
+ @Operation(
+ summary = "Get authenticated user's comments",
+ description = "Returns paginated comments created by the authenticated user"
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "User comments retrieved successfully",
+ content = @Content(schema = @Schema(implementation = CommentDto.class))
+ )
+ @ApiResponse(
+ responseCode = "401",
+ description = "User is not authenticated",
+ content = @Content
+ )
@PatchMapping("/comments/{commentId}")
public CommentDto update(
@PathVariable Long commentId,
@@ -66,6 +126,19 @@ public CommentDto update(
return commentService.update(commentId,requestDto, user.getId());
}
+ @Operation(
+ summary = "Delete a comment",
+ description = "Deletes a comment owned by the authenticated user"
+ )
+ @ApiResponse(
+ responseCode = "204",
+ description = "Comment deleted successfully"
+ )
+ @ApiResponse(
+ responseCode = "403",
+ description = "User is not the owner of the comment",
+ content = @Content
+ )
@DeleteMapping("/comments/{commentId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(
diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java
index ccac842..ef62b38 100644
--- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java
+++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java
@@ -7,6 +7,11 @@
import com.videogamescatalogue.backend.model.Platform;
import com.videogamescatalogue.backend.model.User;
import com.videogamescatalogue.backend.service.game.GameService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
@@ -23,6 +28,7 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+@Tag(name = "Games", description = "Retrieve and search video games")
@RequiredArgsConstructor
@RequestMapping("/games")
@RestController
@@ -31,6 +37,18 @@ public class GameController {
public static final int DEFAULT_PAGE_NUMBER = 1;
private final GameService gameService;
+ @Operation(
+ summary = "Get games stored in local database",
+ description = """
+ Returns paginated games from the local database
+ sorted by API rating starting from the highest
+ """
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Games retrieved successfully",
+ content = @Content(schema = @Schema(implementation = GameDto.class))
+ )
@GetMapping("/local")
public Page getAllGamesFromDb(
@PageableDefault(size = DEFAULT_PAGE_SIZE, sort = "apiRating",
@@ -40,6 +58,33 @@ public Page getAllGamesFromDb(
return gameService.getAllGamesFromDb(pageable);
}
+ @Operation(
+ summary = "Get specific game and its details by API ID",
+ description = """
+ Returns detailed game information using a hybrid DB + external API strategy.
+
+ Behaviour:
+ - If the game exists in the local database:
+ - If it already has a description → data is returned from DB
+ - If description is missing → full data is fetched from external API,
+ saved to DB, and then returned
+ - If the game does not exist in the database:
+ - Data is fetched from external API and returned without saving to DB
+
+ If the user is authenticated, the response also includes
+ user-specific game status.
+ """
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Game retrieved successfully",
+ content = @Content(schema = @Schema(implementation = GameWithStatusDto.class))
+ )
+ @ApiResponse(
+ responseCode = "404",
+ description = "Game not found",
+ content = @Content
+ )
@GetMapping("{gameApiId}")
public GameWithStatusDto getByApiId(
@PathVariable Long gameApiId,
@@ -48,6 +93,15 @@ public GameWithStatusDto getByApiId(
return gameService.getByApiId(gameApiId, user);
}
+ @Operation(
+ summary = "Get games from external API",
+ description = "Returns paginated games fetched from the external games API"
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Games retrieved successfully",
+ content = @Content(schema = @Schema(implementation = GameDto.class))
+ )
@GetMapping
public Page getAllGamesFromApi(
@PageableDefault(size = DEFAULT_PAGE_SIZE, page = DEFAULT_PAGE_NUMBER)
@@ -56,6 +110,20 @@ public Page getAllGamesFromApi(
return gameService.getAllGamesFromApi(pageable);
}
+ @Operation(
+ summary = "Search games in local database",
+ description = "Search games by platforms, genres, name and year"
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Search results returned successfully",
+ content = @Content(schema = @Schema(implementation = GameDto.class))
+ )
+ @ApiResponse(
+ responseCode = "400",
+ description = "Invalid search parameters",
+ content = @Content
+ )
@GetMapping("/local/search")
public Page search(
@ModelAttribute GameSearchParameters searchParameters,
@@ -66,6 +134,15 @@ public Page search(
return gameService.search(searchParameters, pageable);
}
+ @Operation(
+ summary = "Search games via external API",
+ description = "Flexible search using raw query parameters forwarded to the external API"
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Search results returned successfully",
+ content = @Content(schema = @Schema(implementation = GameDto.class))
+ )
@GetMapping("/search")
public Page apiSearch(
@RequestParam Map searchParams
diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserController.java
index af8daf2..17ab850 100644
--- a/src/main/java/com/videogamescatalogue/backend/controller/UserController.java
+++ b/src/main/java/com/videogamescatalogue/backend/controller/UserController.java
@@ -5,6 +5,11 @@
import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto;
import com.videogamescatalogue.backend.model.User;
import com.videogamescatalogue.backend.service.user.UserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -15,12 +20,34 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+@Tag(name = "Users", description = "Operations related to user profiles and accounts")
@RequiredArgsConstructor
@RequestMapping("/users")
@RestController
public class UserController {
private final UserService userService;
+ @Operation(
+ summary = "Get user information by ID",
+ description = " Returns user profile information. Requires authentication"
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "User information retrieved successfully",
+ content = @Content(
+ schema = @Schema(implementation = UserResponseDto.class)
+ )
+ )
+ @ApiResponse(
+ responseCode = "403",
+ description = "Access denied",
+ content = @Content
+ )
+ @ApiResponse(
+ responseCode = "404",
+ description = "User not found",
+ content = @Content
+ )
@GetMapping("/{id}")
public UserResponseDto getUserInfo(
@PathVariable Long id,
@@ -29,6 +56,27 @@ public UserResponseDto getUserInfo(
return userService.getUserInfo(id, user);
}
+ @Operation(
+ summary = "Update authenticated user's profile",
+ description = "Updates profile information of the currently authenticated user"
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "User profile updated successfully",
+ content = @Content(
+ schema = @Schema(implementation = UserResponseDto.class)
+ )
+ )
+ @ApiResponse(
+ responseCode = "400",
+ description = "Validation error",
+ content = @Content
+ )
+ @ApiResponse(
+ responseCode = "401",
+ description = "User is not authenticated",
+ content = @Content
+ )
@PatchMapping("/me")
public UserResponseDto updateUserInfo(
@Valid @RequestBody UpdateUserRequestDto requestDto,
@@ -37,6 +85,28 @@ public UserResponseDto updateUserInfo(
return userService.updateUserInfo(requestDto, user);
}
+ @Operation(
+ summary = "Change authenticated user's password",
+ description = "Changes the password of the currently authenticated user. "
+ + "This operation requires the current password and a new password."
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Password changed successfully",
+ content = @Content(
+ schema = @Schema(implementation = UserResponseDto.class)
+ )
+ )
+ @ApiResponse(
+ responseCode = "400",
+ description = "Invalid current password or validation error",
+ content = @Content
+ )
+ @ApiResponse(
+ responseCode = "401",
+ description = "User is not authenticated",
+ content = @Content
+ )
@PatchMapping("/me/password")
public UserResponseDto changePassword(
@Valid @RequestBody ChangePasswordRequestDto requestDto,
diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java
index 517119b..fcfc227 100644
--- a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java
+++ b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java
@@ -5,6 +5,11 @@
import com.videogamescatalogue.backend.model.User;
import com.videogamescatalogue.backend.model.UserGame;
import com.videogamescatalogue.backend.service.usergame.UserGameService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -19,6 +24,7 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+@Tag(name = "User Games", description = "Manage games in the authenticated user's library")
@RequiredArgsConstructor
@RequestMapping("/user-games")
@RestController
@@ -26,6 +32,34 @@ public class UserGameController {
public static final int DEFAULT_PAGE_SIZE = 30;
private final UserGameService userGameService;
+ @Operation(
+ summary = "Add or update a game in user's library",
+ description = """
+ Creates or updates a game entry in the authenticated user's library.
+
+ Behaviour:
+ - If the game already exists in the user's library → its status is updated
+ - If the game does not exist → a new entry is created
+
+ """
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "User game created or updated successfully",
+ content = @Content(
+ schema = @Schema(implementation = UserGameDto.class)
+ )
+ )
+ @ApiResponse(
+ responseCode = "400",
+ description = "Invalid request data",
+ content = @Content
+ )
+ @ApiResponse(
+ responseCode = "401",
+ description = "User is not authenticated",
+ content = @Content
+ )
@PostMapping
public UserGameDto createOrUpdate(
@RequestBody CreateUserGameDto createDto,
@@ -34,6 +68,28 @@ public UserGameDto createOrUpdate(
return userGameService.createOrUpdate(createDto, user);
}
+ @Operation(
+ summary = "Remove a game from user's library",
+ description = """
+ Deletes a game entry from the authenticated user's library.
+
+ Only the owner of the entry can delete it.
+ """
+ )
+ @ApiResponse(
+ responseCode = "204",
+ description = "User game deleted successfully"
+ )
+ @ApiResponse(
+ responseCode = "403",
+ description = "User is not allowed to delete this entry",
+ content = @Content
+ )
+ @ApiResponse(
+ responseCode = "404",
+ description = "User game not found",
+ content = @Content
+ )
@DeleteMapping("/{id}")
public void delete(
@PathVariable Long id,
@@ -42,6 +98,25 @@ public void delete(
userGameService.delete(id, user);
}
+ @Operation(
+ summary = "Get user's games by status",
+ description = """
+ Returns paginated games from the authenticated
+ user's library filtered by game status
+ """
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "User games retrieved successfully",
+ content = @Content(
+ schema = @Schema(implementation = UserGameDto.class)
+ )
+ )
+ @ApiResponse(
+ responseCode = "401",
+ description = "User is not authenticated",
+ content = @Content
+ )
@GetMapping
public Page getByStatus(
@RequestParam UserGame.GameStatus status,
diff --git a/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java b/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java
new file mode 100644
index 0000000..980ea33
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java
@@ -0,0 +1,162 @@
+package com.videogamescatalogue.backend.exception;
+
+import java.time.LocalDateTime;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.HttpStatusCode;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.validation.FieldError;
+import org.springframework.validation.ObjectError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+@Log4j2
+@ControllerAdvice
+public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
+ @Override
+ protected ResponseEntity
diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java
index 8a04bd2..589f8d4 100644
--- a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java
+++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/CommentDto.java
@@ -5,7 +5,9 @@
public record CommentDto(
Long id,
Long gameApiId,
+ String gameName,
Long userId,
+ String profileName,
String text,
LocalDateTime localDateTime,
Integer rating
diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/comment/CommentMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/comment/CommentMapper.java
index 2504425..4b34909 100644
--- a/src/main/java/com/videogamescatalogue/backend/mapper/comment/CommentMapper.java
+++ b/src/main/java/com/videogamescatalogue/backend/mapper/comment/CommentMapper.java
@@ -4,6 +4,7 @@
import com.videogamescatalogue.backend.dto.internal.comment.CommentDto;
import com.videogamescatalogue.backend.dto.internal.comment.CreateCommentRequestDto;
import com.videogamescatalogue.backend.model.Comment;
+import com.videogamescatalogue.backend.model.Game;
import com.videogamescatalogue.backend.model.User;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@@ -13,11 +14,23 @@
public interface CommentMapper {
Comment toModel(CreateCommentRequestDto requestDto);
- @Mapping(source = "user", target = "userId", qualifiedByName = "toUserId")
+ @Mapping(target = "gameName", source = "game", qualifiedByName = "toGameName")
+ @Mapping(target = "userId", source = "user", qualifiedByName = "toUserId")
+ @Mapping(target = "profileName", source = "user", qualifiedByName = "toProfileName")
CommentDto toDto(Comment comment);
@Named("toUserId")
default Long toUserId(User user) {
return user.getId();
}
+
+ @Named("toGameName")
+ default String toGameName(Game game) {
+ return game.getName();
+ }
+
+ @Named("toProfileName")
+ default String toProfileName(User user) {
+ return user.getProfileName();
+ }
}
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java
index 5e5a4df..ad5ee03 100644
--- a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java
+++ b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java
@@ -20,7 +20,7 @@ public Specification build(GameSearchParameters searchParameters) {
if (searchParameters == null) {
throw new IllegalArgumentException("Search Parameters cannot be null");
}
- Specification specification = Specification.where((Specification) null);
+ Specification specification = Specification.where(null);
specification = getSpecificationForParam(
searchParameters.name(),
Game.SpecificationKey.NAME.getValue(), specification
diff --git a/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java
index 9ef4ac4..4b7a9c0 100644
--- a/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java
+++ b/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java
@@ -107,7 +107,8 @@ void setUp() {
comment.setUser(user);
commentDto = new CommentDto(
- 1L, 1L, 10L, "comment text",
+ 1L, 1L, "GameName", 10L,
+ "user", "comment text",
LocalDateTime.now(), 5
);
@@ -122,7 +123,8 @@ void setUp() {
);
commentDtoUpdated = new CommentDto(
- 2L, 1L, 10L, "comment text updated",
+ 2L, 1L, "GameName", 10L,
+ "user", "comment text updated",
LocalDateTime.now(), 5
);
}
From 91eb2758ef930eb4b0eaea83e837788f2f9a2891 Mon Sep 17 00:00:00 2001
From: Alina Bondarenko
Date: Mon, 5 Jan 2026 19:01:22 +0200
Subject: [PATCH 32/41] webhook after ci (#44)
---
.github/workflows/backend-webhook.yml | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/backend-webhook.yml b/.github/workflows/backend-webhook.yml
index 94a3b79..8a9870a 100644
--- a/.github/workflows/backend-webhook.yml
+++ b/.github/workflows/backend-webhook.yml
@@ -1,13 +1,17 @@
name: Trigger Build on Main Repo
on:
- push:
+ workflow_run:
+ workflows: ["Java CI"]
+ types:
+ - completed
branches:
- main
jobs:
notify:
runs-on: ubuntu-latest
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Trigger build in main repository
run: |
@@ -15,9 +19,9 @@ jobs:
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
https://api.github.com/repos/Bondaliname/my-project-infrastructure/dispatches \
- -d '{"event_type":"backend-updated","client_payload":{"ref":"${{ github.ref }}","sha":"${{ github.sha }}"}}'
-
+ -d '{"event_type":"backend-updated","client_payload":{"ref":"refs/heads/${{ github.event.workflow_run.head_branch }}","sha":"${{ github.event.workflow_run.head_sha }}"}}'
+
- name: Summary
run: |
- echo "Commit: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
- echo "Branch: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
+ echo "Commit: ${{ github.event.workflow_run.head_sha }}" >> $GITHUB_STEP_SUMMARY
+ echo "Branch: ${{ github.event.workflow_run.head_branch }}" >> $GITHUB_STEP_SUMMARY
From ac19eac31c324fa5acfbda934e31169ab92d376b Mon Sep 17 00:00:00 2001
From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com>
Date: Wed, 7 Jan 2026 14:03:10 +0200
Subject: [PATCH 33/41] Update public endpoints (#46)
* made getUserInfo public
* made getUserComments public
* made getByStatus userGame public
* ran mvn clean package
---
README.md | 10 +++---
.../backend/config/SecurityConfig.java | 14 +++++---
.../backend/controller/CommentController.java | 9 +++--
.../backend/controller/UserController.java | 7 ++--
.../controller/UserGameController.java | 5 +--
.../AuthenticationRequiredException.java | 7 ++++
.../CustomGlobalExceptionHandler.java | 9 +++++
.../service/comment/CommentService.java | 2 +-
.../service/comment/CommentServiceImpl.java | 15 +++++++-
.../backend/service/user/UserService.java | 4 +--
.../backend/service/user/UserServiceImpl.java | 35 +++++++++++--------
.../service/usergame/UserGameService.java | 1 +
.../service/usergame/UserGameServiceImpl.java | 16 +++++++++
.../comment/CommentServiceImplTest.java | 2 +-
.../service/user/UserServiceImplTest.java | 33 ++++++++++++++---
15 files changed, 126 insertions(+), 43 deletions(-)
create mode 100644 src/main/java/com/videogamescatalogue/backend/exception/AuthenticationRequiredException.java
diff --git a/README.md b/README.md
index c1eb05c..ec81aaa 100644
--- a/README.md
+++ b/README.md
@@ -187,10 +187,10 @@ _ Description: Authenticates a user and returns JWT token.
- Authentication: Not required
### getUserComments
-- Description: Returns a paginated list of comments created by the authenticated user.
+- Description: Returns a paginated list of comments created by the authenticated user or a user by id.
- URL: http://localhost:8080/api/comments
- Method: GET
-- Authentication: Required
+- Authentication: Not required
### updateComment
- Description: Updates an existing comment owned by the authenticated user.
@@ -207,11 +207,11 @@ _ Description: Authenticates a user and returns JWT token.
## UserController
### getUserInfo
- Description: Returns user information. If id is provided, returns info by id.
- If id is not provided, returns info about authenticated user. Any authenticated user can see other user's profile info.
+ If id is not provided, returns info about authenticated user. Any user can see other user's profile info.
- URL: http://localhost:8080/api/users/info
- Method: GET
- @RequestParam(required = false) Long id
-- Authentication: Required
+- Authentication: not required
### updateUserInfo
- Description: Updates the authenticated user’s profile information (profileName, email, about, location).
@@ -242,7 +242,7 @@ _ Description: Authenticates a user and returns JWT token.
- Description: Returns a paginated list of any user’s games filtered by status.
- URL: http://localhost:8080/api/user-games
- Method: GET
-- Authentication: Required
+- Authentication: Not required
## AdminGameController
Provides administrative endpoints for managing game data fetched from an external API. These endpoints are intended only for administrators and are protected by role-based security.
diff --git a/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java b/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java
index 4f3e2eb..d728510 100644
--- a/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java
+++ b/src/main/java/com/videogamescatalogue/backend/config/SecurityConfig.java
@@ -4,6 +4,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
@@ -23,8 +24,13 @@
@RequiredArgsConstructor
@Configuration
public class SecurityConfig {
- public static final String[] PUBLIC_ENDPOINTS = {"/auth/**", "/games/**", "/error",
- "/swagger-ui/**", "/v3/api-docs/**", "/actuator/health/**"};
+ public static final String[] PUBLIC_ENDPOINTS = {
+ "/auth/**", "/games/**", "/users/info/**", "/error",
+ "/swagger-ui/**", "/v3/api-docs/**", "/actuator/health/**"
+ };
+ public static final String[] GET_PUBLIC_ENDPOINTS = {
+ "/user-games/**", "/comments/**"
+ };
private final UserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@@ -40,8 +46,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(
auth -> auth
- .requestMatchers(PUBLIC_ENDPOINTS)
- .permitAll()
+ .requestMatchers(PUBLIC_ENDPOINTS).permitAll()
+ .requestMatchers(HttpMethod.GET, GET_PUBLIC_ENDPOINTS).permitAll()
.anyRequest()
.authenticated()
)
diff --git a/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java b/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java
index f26275c..42e5aed 100644
--- a/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java
+++ b/src/main/java/com/videogamescatalogue/backend/controller/CommentController.java
@@ -24,6 +24,7 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@@ -81,8 +82,9 @@ public Page getCommentsForGame(
}
@Operation(
- summary = "Get authenticated user's comments",
- description = "Returns paginated comments created by the authenticated user"
+ summary = "Get usercomments",
+ description = "Returns paginated comments created by "
+ + "the authenticated user or a user by id"
)
@ApiResponse(
responseCode = "200",
@@ -96,11 +98,12 @@ public Page getCommentsForGame(
)
@GetMapping("/comments")
public Page getUserComments(
+ @RequestParam(required = false) Long id,
@AuthenticationPrincipal User user,
@PageableDefault(size = DEFAULT_PAGE_SIZE)
Pageable pageable
) {
- return commentService.getUserComments(user.getId(), pageable);
+ return commentService.getUserComments(user, id, pageable);
}
@Operation(
diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserController.java
index 8edb8a4..d5dcda8 100644
--- a/src/main/java/com/videogamescatalogue/backend/controller/UserController.java
+++ b/src/main/java/com/videogamescatalogue/backend/controller/UserController.java
@@ -33,7 +33,7 @@ public class UserController {
Returns user profile information.
If id is provided, returns info by id.
If id is not provided, returns info about authenticated user.
- Requires authentication
+ Does not require authentication
"""
)
@ApiResponse(
@@ -58,10 +58,7 @@ public UserResponseDto getUserInfo(
@RequestParam(required = false) Long id,
@AuthenticationPrincipal User user
) {
- if (id == null) {
- return userService.getAuthenticatedUserInfo(user);
- }
- return userService.getUserInfoById(id, user);
+ return userService.getUserInfo(id, user);
}
@Operation(
diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java
index c4b8699..402ac6b 100644
--- a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java
+++ b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java
@@ -123,12 +123,13 @@ public void delete(
@GetMapping
public Page getByStatus(
@RequestParam UserGame.GameStatus status,
- @AuthenticationPrincipal User user,
+ @RequestParam(required = false) Long userId,
+ @AuthenticationPrincipal User authenticatedUser,
@PageableDefault(size = DEFAULT_PAGE_SIZE)
Pageable pageable
) {
return userGameService.getByStatus(
- status, user.getId(), pageable
+ status, userId, authenticatedUser, pageable
);
}
}
diff --git a/src/main/java/com/videogamescatalogue/backend/exception/AuthenticationRequiredException.java b/src/main/java/com/videogamescatalogue/backend/exception/AuthenticationRequiredException.java
new file mode 100644
index 0000000..fa6e9d9
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/exception/AuthenticationRequiredException.java
@@ -0,0 +1,7 @@
+package com.videogamescatalogue.backend.exception;
+
+public class AuthenticationRequiredException extends RuntimeException {
+ public AuthenticationRequiredException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java b/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java
index 980ea33..48de145 100644
--- a/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java
+++ b/src/main/java/com/videogamescatalogue/backend/exception/CustomGlobalExceptionHandler.java
@@ -146,6 +146,15 @@ protected ResponseEntity handleSpecificationProviderNotFoundException(
HttpStatus.NOT_FOUND);
}
+ @ExceptionHandler(AuthenticationRequiredException.class)
+ protected ResponseEntity handleAuthenticationRequiredException(
+ AuthenticationRequiredException ex
+ ) {
+ log.info("Authentication Required error", ex);
+ return new ResponseEntity<>(getBody(List.of(ex.getMessage())),
+ HttpStatus.UNAUTHORIZED);
+ }
+
private String getErrorMessage(ObjectError error) {
if (error instanceof FieldError fieldError) {
return fieldError.getField() + ": " + error.getDefaultMessage();
diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java
index 6b0e156..e70fb77 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentService.java
@@ -16,7 +16,7 @@ CommentDto create(
Page getCommentsForGame(Long gameApiId, Pageable pageable);
- Page getUserComments(Long userId, Pageable pageable);
+ Page getUserComments(User authenticatedUser, Long userId, Pageable pageable);
CommentDto update(Long commentId, UpdateCommentRequestDto requestDto, Long userId);
diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java
index 45949fd..8c15d2d 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java
@@ -5,6 +5,7 @@
import com.videogamescatalogue.backend.dto.internal.comment.CreateCommentRequestDto;
import com.videogamescatalogue.backend.dto.internal.comment.UpdateCommentRequestDto;
import com.videogamescatalogue.backend.exception.AccessNotAllowedException;
+import com.videogamescatalogue.backend.exception.AuthenticationRequiredException;
import com.videogamescatalogue.backend.exception.EntityNotFoundException;
import com.videogamescatalogue.backend.mapper.comment.CommentMapper;
import com.videogamescatalogue.backend.mapper.game.GameMapper;
@@ -48,7 +49,19 @@ public Page getCommentsForGame(Long gameApiId, Pageable pageable) {
}
@Override
- public Page getUserComments(Long userId, Pageable pageable) {
+ public Page getUserComments(
+ User authenticatedUser, Long userId, Pageable pageable
+ ) {
+ if (authenticatedUser == null && userId == null) {
+ throw new AuthenticationRequiredException("Authentication is required");
+ }
+ if (userId == null) {
+ return findCommentsByUserId(authenticatedUser.getId(), pageable);
+ }
+ return findCommentsByUserId(userId, pageable);
+ }
+
+ private Page findCommentsByUserId(Long userId, Pageable pageable) {
Page userComments = commentRepository.findAllByUserId(userId, pageable);
return userComments.map(commentMapper::toDto);
}
diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java
index f53525c..73c69ac 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java
@@ -11,9 +11,7 @@ UserResponseDto registerUser(
UserRegistrationRequestDto requestDto
);
- UserResponseDto getUserInfoById(Long userId, User authenticatedUser);
-
- UserResponseDto getAuthenticatedUserInfo(User authenticatedUser);
+ UserResponseDto getUserInfo(Long userId, User authenticatedUser);
UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User authenticatedUser);
diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java
index 9b64dbc..5de302c 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java
@@ -4,6 +4,7 @@
import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto;
import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto;
import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto;
+import com.videogamescatalogue.backend.exception.AuthenticationRequiredException;
import com.videogamescatalogue.backend.exception.EntityNotFoundException;
import com.videogamescatalogue.backend.exception.InvalidInputException;
import com.videogamescatalogue.backend.exception.RegistrationException;
@@ -52,21 +53,14 @@ public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) {
}
@Override
- public UserResponseDto getUserInfoById(Long userId, User authenticatedUser) {
- if (authenticatedUser.getId().equals(userId)) {
- return userMapper.toDto(authenticatedUser);
+ public UserResponseDto getUserInfo(Long userId, User authenticatedUser) {
+ if (authenticatedUser == null && userId == null) {
+ throw new AuthenticationRequiredException("Authentication is required");
}
- User user = userRepository.findById(userId).orElseThrow(
- () -> new EntityNotFoundException(
- "There is no user by id: " + userId
- )
- );
- return userMapper.toDto(user);
- }
-
- @Override
- public UserResponseDto getAuthenticatedUserInfo(User authenticatedUser) {
- return userMapper.toDto(authenticatedUser);
+ if (userId == null) {
+ return getAuthenticatedUserInfo(authenticatedUser);
+ }
+ return getUserInfoById(userId);
}
@Override
@@ -117,4 +111,17 @@ private void checkUserAlreadyExists(String email) {
+ email + " is already registered.");
}
}
+
+ private UserResponseDto getUserInfoById(Long userId) {
+ User user = userRepository.findById(userId).orElseThrow(
+ () -> new EntityNotFoundException(
+ "There is no user by id: " + userId
+ )
+ );
+ return userMapper.toDto(user);
+ }
+
+ private UserResponseDto getAuthenticatedUserInfo(User authenticatedUser) {
+ return userMapper.toDto(authenticatedUser);
+ }
}
diff --git a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java
index f5636fa..8f3844b 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java
@@ -15,6 +15,7 @@ public interface UserGameService {
Page getByStatus(
UserGame.GameStatus status,
Long userId,
+ User authenticatedUser,
Pageable pageable
);
}
diff --git a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java
index ae75c5d..1c0c19c 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java
@@ -4,6 +4,7 @@
import com.videogamescatalogue.backend.dto.internal.usergame.CreateUserGameDto;
import com.videogamescatalogue.backend.dto.internal.usergame.UserGameDto;
import com.videogamescatalogue.backend.exception.AccessNotAllowedException;
+import com.videogamescatalogue.backend.exception.AuthenticationRequiredException;
import com.videogamescatalogue.backend.exception.EntityNotFoundException;
import com.videogamescatalogue.backend.mapper.game.GameMapper;
import com.videogamescatalogue.backend.mapper.usergame.UserGameMapper;
@@ -61,7 +62,22 @@ public void delete(Long id, User user) {
public Page getByStatus(
UserGame.GameStatus status,
Long userId,
+ User authenticatedUser,
Pageable pageable) {
+ if (authenticatedUser == null && userId == null) {
+ throw new AuthenticationRequiredException("Authentication is required");
+ }
+ if (userId == null) {
+ return findByUserIdAndStatus(
+ authenticatedUser.getId(), status, pageable
+ );
+ }
+ return findByUserIdAndStatus(userId,status,pageable);
+ }
+
+ private Page findByUserIdAndStatus(
+ Long userId, UserGame.GameStatus status, Pageable pageable
+ ) {
Page userGames = userGameRepository.findByUserIdAndStatus(
userId, status, pageable
);
diff --git a/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java
index 4b7a9c0..966b0b0 100644
--- a/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java
+++ b/src/test/java/com/videogamescatalogue/backend/service/comment/CommentServiceImplTest.java
@@ -179,7 +179,7 @@ void getUserComments_validRequest_returnPageCommentDto() {
.thenReturn(commentPage);
when(commentMapper.toDto(comment)).thenReturn(commentDto);
- Page actual = commentService.getUserComments(userId, pageable);
+ Page actual = commentService.getUserComments(user, userId, pageable);
assertNotNull(actual);
assertEquals(commentDtoPage, actual);
diff --git a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java
index 82bafe1..8d7ef53 100644
--- a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java
+++ b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java
@@ -14,6 +14,7 @@
import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto;
import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto;
import com.videogamescatalogue.backend.dto.internal.usergame.UserGameStatusDto;
+import com.videogamescatalogue.backend.exception.AuthenticationRequiredException;
import com.videogamescatalogue.backend.exception.InvalidInputException;
import com.videogamescatalogue.backend.exception.RegistrationException;
import com.videogamescatalogue.backend.mapper.user.UserMapper;
@@ -151,15 +152,39 @@ void registerUser_validRequest_returnUserDto() {
}
@Test
- void getUserInfo_usersMatch_returnAuthenticatedUserInfo() {
+ void getUserInfo_onlyAuthUser_returnAuthenticatedUserInfo() {
when(userMapper.toDto(user))
+ .thenReturn(responseDtoUser);
+
+ UserResponseDto actual = userService.getUserInfo(null, user);
+
+ assertEquals(responseDtoUser, actual);
+
+ verifyNoInteractions(userRepository);
+ }
+
+ @Test
+ void getUserInfo_onlyUserId_returnUserInfoById() {
+ when(userRepository.findById(9L))
+ .thenReturn(Optional.ofNullable(userBob));
+ when(userMapper.toDto(userBob))
.thenReturn(responseDtoBob);
- UserResponseDto actual = userService.getUserInfoById(10L, user);
+ UserResponseDto actual = userService.getUserInfo(9L, null);
assertEquals(responseDtoBob, actual);
- verifyNoInteractions(userRepository);
+ verify(userRepository).findById(9L);
+ }
+
+ @Test
+ void getUserInfo_thUserNoId_throwException() {
+ assertThrows(
+ AuthenticationRequiredException.class,
+ () -> userService.getUserInfo(null, null)
+ );
+
+ verifyNoInteractions(userRepository, userMapper);
}
@Test
@@ -169,7 +194,7 @@ void getUserInfo_usersDoNotMatch_returnOtherUserInfo() {
when(userMapper.toDto(userBob))
.thenReturn(responseDtoBob);
- UserResponseDto actual = userService.getUserInfoById(9L, user);
+ UserResponseDto actual = userService.getUserInfo(9L, user);
assertEquals(responseDtoBob, actual);
assertNotEquals(responseDtoUser, actual);
From de8e901bc17846de16c4633d3cb4a1c32b9351a4 Mon Sep 17 00:00:00 2001
From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com>
Date: Sat, 10 Jan 2026 17:06:52 +0200
Subject: [PATCH 34/41] refactored delete method (#48)
---
README.md | 2 +-
.../controller/UserGameController.java | 6 ++---
.../repository/UserGameRepository.java | 2 +-
.../backend/service/game/GameServiceImpl.java | 2 +-
.../service/usergame/UserGameService.java | 2 +-
.../service/usergame/UserGameServiceImpl.java | 25 ++++++++-----------
src/main/resources/application.properties | 2 ++
.../usergame/UserGameServiceImplTest.java | 24 +++++++++++-------
8 files changed, 35 insertions(+), 30 deletions(-)
diff --git a/README.md b/README.md
index ec81aaa..a9a0ddb 100644
--- a/README.md
+++ b/README.md
@@ -234,7 +234,7 @@ _ Description: Authenticates a user and returns JWT token.
### deleteUserGame
- Description: Removes a game from the authenticated user’s list.
-- URL: http://localhost:8080/api/user-games/{id}
+- URL: http://localhost:8080/api/user-games/{apiId}
- Method: DELETE
- Authentication: Required
diff --git a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java
index 402ac6b..ca3f434 100644
--- a/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java
+++ b/src/main/java/com/videogamescatalogue/backend/controller/UserGameController.java
@@ -92,13 +92,13 @@ public UserGameDto createOrUpdate(
description = "User game not found",
content = @Content
)
- @DeleteMapping("/{id}")
+ @DeleteMapping("/{apiId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(
- @PathVariable Long id,
+ @PathVariable Long apiId,
@AuthenticationPrincipal User user
) {
- userGameService.delete(id, user);
+ userGameService.delete(apiId, user);
}
@Operation(
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java
index cf93e15..1e281cf 100644
--- a/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java
+++ b/src/main/java/com/videogamescatalogue/backend/repository/UserGameRepository.java
@@ -8,7 +8,7 @@
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserGameRepository extends JpaRepository {
- Optional findByUserIdAndGameApiId(Long userId, Long apiId);
+ Optional findByUser_IdAndGame_ApiId(Long userId, Long apiId);
List findAllByUserId(Long userId);
diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java
index 71a536b..8671744 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java
@@ -140,7 +140,7 @@ private UserGame.GameStatus getGameStatus(Long apiId, User user) {
return null;
}
- Optional userGameOptional = userGameRepository.findByUserIdAndGameApiId(
+ Optional userGameOptional = userGameRepository.findByUser_IdAndGame_ApiId(
user.getId(), apiId
);
return userGameOptional.map(UserGame::getStatus)
diff --git a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java
index 8f3844b..23e9f01 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameService.java
@@ -10,7 +10,7 @@
public interface UserGameService {
UserGameDto createOrUpdate(CreateUserGameDto createDto, User user);
- void delete(Long id, User user);
+ void delete(Long apiId, User user);
Page getByStatus(
UserGame.GameStatus status,
diff --git a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java
index 1c0c19c..e498194 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImpl.java
@@ -3,7 +3,6 @@
import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto;
import com.videogamescatalogue.backend.dto.internal.usergame.CreateUserGameDto;
import com.videogamescatalogue.backend.dto.internal.usergame.UserGameDto;
-import com.videogamescatalogue.backend.exception.AccessNotAllowedException;
import com.videogamescatalogue.backend.exception.AuthenticationRequiredException;
import com.videogamescatalogue.backend.exception.EntityNotFoundException;
import com.videogamescatalogue.backend.mapper.game.GameMapper;
@@ -31,7 +30,7 @@ public class UserGameServiceImpl implements UserGameService {
@Override
public UserGameDto createOrUpdate(CreateUserGameDto createDto, User user) {
- Optional userGameOptional = userGameRepository.findByUserIdAndGameApiId(
+ Optional userGameOptional = userGameRepository.findByUser_IdAndGame_ApiId(
user.getId(),
createDto.apiId()
);
@@ -53,9 +52,9 @@ public UserGameDto createOrUpdate(CreateUserGameDto createDto, User user) {
}
@Override
- public void delete(Long id, User user) {
- checkBelongsToUser(id, user.getId());
- userGameRepository.deleteById(id);
+ public void delete(Long apiId, User user) {
+ UserGame userGame = getUserGame(user.getId(), apiId);
+ userGameRepository.delete(userGame);
}
@Override
@@ -84,17 +83,15 @@ private Page findByUserIdAndStatus(
return userGames.map(userGameMapper::toDto);
}
- private void checkBelongsToUser(Long id, Long userId) {
- Optional userGameOptional = userGameRepository.findById(id);
+ private UserGame getUserGame(Long userId, Long apiId) {
+ Optional userGameOptional = userGameRepository.findByUser_IdAndGame_ApiId(
+ userId, apiId
+ );
if (userGameOptional.isEmpty()) {
- throw new EntityNotFoundException("There is no userGame by id: " + id);
- }
-
- Long userGameUserId = userGameOptional.get().getUser().getId();
- if (!userGameUserId.equals(userId)) {
- throw new AccessNotAllowedException("User with id: "
- + userId + "is not allowed to access userGame with id: " + id);
+ throw new EntityNotFoundException("There is no userGame by apiId: " + apiId
+ + " for user with id: " + userId);
}
+ return userGameOptional.get();
}
private UserGameDto addGameAndSave(UserGame userGame, Game game) {
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index ca164fd..c870717 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -9,6 +9,8 @@ spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.liquibase.enabled=true
spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml
+spring.jpa.show-sql=true
+
spring.jpa.hibernate.ddl-auto=none
server.servlet.context-path=/api
diff --git a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java
index adf6300..441d9d7 100644
--- a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java
+++ b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java
@@ -68,9 +68,11 @@ void setUp() {
user.setLocation("location");
user.getRoles().add(role);
+ Game game = new Game();
+ game.setApiId(100L);
userGame = new UserGame();
userGame.setUser(user);
- userGame.setGame(new Game());
+ userGame.setGame(game);
userGame.setStatus(UserGame.GameStatus.BACKLOG);
platformDto = new PlatformDto("PC");
@@ -94,7 +96,7 @@ void setUp() {
@Test
void createOrUpdate_alreadyExists_update() {
- when(userGameRepository.findByUserIdAndGameApiId(
+ when(userGameRepository.findByUser_IdAndGame_ApiId(
user.getId(),
createUserGameDto.apiId()
)).thenReturn(Optional.of(userGame));
@@ -110,7 +112,7 @@ void createOrUpdate_alreadyExists_update() {
assertNotNull(actual);
assertEquals(userGameDto, actual);
- verify(userGameRepository).findByUserIdAndGameApiId(
+ verify(userGameRepository).findByUser_IdAndGame_ApiId(
user.getId(),
createUserGameDto.apiId()
);
@@ -119,7 +121,7 @@ void createOrUpdate_alreadyExists_update() {
@Test
void createOrUpdate_newUserGameAndGameInDb_createUserGameDto() {
- when(userGameRepository.findByUserIdAndGameApiId(
+ when(userGameRepository.findByUser_IdAndGame_ApiId(
user.getId(),
createUserGameDto.apiId()
)).thenReturn(Optional.empty());
@@ -140,7 +142,7 @@ void createOrUpdate_newUserGameAndGameInDb_createUserGameDto() {
assertNotNull(actual);
assertEquals(userGameDto, actual);
- verify(userGameRepository).findByUserIdAndGameApiId(
+ verify(userGameRepository).findByUser_IdAndGame_ApiId(
user.getId(),
createUserGameDto.apiId()
);
@@ -150,14 +152,18 @@ void createOrUpdate_newUserGameAndGameInDb_createUserGameDto() {
@Test
void delete_validRequest_delete() {
Long userGameId = userGame.getId();
- when(userGameRepository.findById(userGameId))
+ when(userGameRepository.findByUser_IdAndGame_ApiId(
+ user.getId(), userGame.getGame().getApiId()
+ ))
.thenReturn(Optional.of(userGame));
assertDoesNotThrow(() -> userGameService.delete(
- userGameId, user
+ userGame.getGame().getApiId(), user
));
- verify(userGameRepository).findById(userGameId);
- verify(userGameRepository).deleteById(userGameId);
+ verify(userGameRepository).findByUser_IdAndGame_ApiId(
+ user.getId(), userGame.getGame().getApiId()
+ );
+ verify(userGameRepository).delete(userGame);
}
}
From 724f9c3b53738af7a038f5d7bd754a1cd257ffa1 Mon Sep 17 00:00:00 2001
From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com>
Date: Sun, 11 Jan 2026 11:46:39 +0200
Subject: [PATCH 35/41] made filtering games by several years (#49)
---
.../backend/dto/internal/GameSearchParameters.java | 2 +-
.../com/videogamescatalogue/backend/model/Game.java | 2 +-
.../repository/game/GameSpecificationBuilder.java | 4 ++--
.../game/spec/GamePlatformsSpecificationProvider.java | 4 ++--
.../game/spec/GameYearSpecificationProvider.java | 10 ++++++----
5 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java
index ad56359..c85104d 100644
--- a/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java
+++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/GameSearchParameters.java
@@ -4,7 +4,7 @@
public record GameSearchParameters(
String name,
- Integer year,
+ List years,
List platforms,
List genres
) {
diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java
index e4eaa2e..449fda0 100644
--- a/src/main/java/com/videogamescatalogue/backend/model/Game.java
+++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java
@@ -67,7 +67,7 @@ public class Game {
@Getter
public enum SpecificationKey {
NAME("name"),
- YEAR("year"),
+ YEARS("years"),
PLATFORMS("platforms"),
GENRES("genres");
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java
index ad5ee03..4113159 100644
--- a/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java
+++ b/src/main/java/com/videogamescatalogue/backend/repository/game/GameSpecificationBuilder.java
@@ -26,8 +26,8 @@ public Specification build(GameSearchParameters searchParameters) {
Game.SpecificationKey.NAME.getValue(), specification
);
specification = getSpecificationForParam(
- searchParameters.year(),
- Game.SpecificationKey.YEAR.getValue(), specification
+ searchParameters.years(),
+ Game.SpecificationKey.YEARS.getValue(), specification
);
specification = getSpecificationForParam(
searchParameters.platforms(),
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java
index c72ceae..e61c41f 100644
--- a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java
+++ b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GamePlatformsSpecificationProvider.java
@@ -39,9 +39,9 @@ public Predicate toPredicate(
.map(Platform.GeneralName::valueOf)
.toList();
- Join platromJoin = root.join(KEY);
+ Join platformJoin = root.join(KEY);
- return platromJoin.get("generalName").in(names);
+ return platformJoin.get("generalName").in(names);
}
};
}
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java
index 7ed36b2..6936fbb 100644
--- a/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java
+++ b/src/main/java/com/videogamescatalogue/backend/repository/game/spec/GameYearSpecificationProvider.java
@@ -6,13 +6,14 @@
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
+import java.util.List;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Component;
@Component
-public class GameYearSpecificationProvider implements SpecificationProvider {
+public class GameYearSpecificationProvider implements SpecificationProvider> {
- public static final String KEY = Game.SpecificationKey.YEAR.getValue();
+ public static final String KEY = Game.SpecificationKey.YEARS.getValue();
@Override
public String getKey() {
@@ -20,7 +21,7 @@ public String getKey() {
}
@Override
- public Specification getSpecification(Integer param) {
+ public Specification getSpecification(List years) {
return new Specification() {
@Override
public Predicate toPredicate(
@@ -28,7 +29,8 @@ public Predicate toPredicate(
CriteriaQuery> query,
CriteriaBuilder criteriaBuilder
) {
- return criteriaBuilder.equal(root.get(KEY), param);
+
+ return root.get("year").in(years);
}
};
}
From e211d8ddd4fb428864881cc5a94bc5f97d6f39d6 Mon Sep 17 00:00:00 2001
From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com>
Date: Mon, 19 Jan 2026 18:27:58 +0200
Subject: [PATCH 36/41] allowed rating to be up to 10 (#51)
---
.../backend/dto/internal/comment/UpdateCommentRequestDto.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java
index 5c5d3a9..26db09c 100644
--- a/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java
+++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/comment/UpdateCommentRequestDto.java
@@ -8,7 +8,7 @@ public record UpdateCommentRequestDto(
@Size(max = 2000, message = "Comment must be less than 2000 digits")
String text,
@Min(0)
- @Max(5)
+ @Max(10)
Integer rating
) {
}
From 05ebf887a6172303cf6765b100dd38f11a76ad2d Mon Sep 17 00:00:00 2001
From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com>
Date: Tue, 20 Jan 2026 18:10:33 +0200
Subject: [PATCH 37/41] added regex to validate (#53)
---
.../internal/user/UserRegistrationRequestDto.java | 15 ++++++++++++---
.../backend/repository/UserRepository.java | 2 ++
.../backend/service/user/UserServiceImpl.java | 12 ++++++++++--
3 files changed, 24 insertions(+), 5 deletions(-)
diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java
index 3c4b168..40a07a7 100644
--- a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java
+++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationRequestDto.java
@@ -3,6 +3,7 @@
import com.videogamescatalogue.backend.validation.FieldMatch;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
@FieldMatch(first = "password",
@@ -10,15 +11,23 @@
message = "Password and repeat password must match")
public record UserRegistrationRequestDto(
@NotBlank(message = "Username is required. Please provide username.")
+ @Pattern(regexp = "^[A-Za-z0-9_]{3,20}$",
+ message = "Profile name may contain only letters, digits, and underscore "
+ + "and be between 3 and 20 digits")
String profileName,
@NotBlank(message = "Password is required. Please provide your password.")
- @Size(min = 8, max = 25, message = "Password must be between 8 and 25 digits")
+ @Pattern(regexp = "^(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?])"
+ + "[A-Za-z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?]{8,30}$",
+ message = "Password must include uppercase, digit, and special character"
+ + "and be between 8 and 30 digits")
String password,
@NotBlank(message = "Please, repeat your password.")
- @Size(min = 8, max = 25, message = "Repeat password must be between 8 and 25 digits")
+ @Size(min = 8, max = 30, message = "Repeat password must be between 8 and 30 digits")
String repeatPassword,
- @Email(message = "Invalid format of email.")
+ @Email(message = "Invalid format of email.",
+ regexp = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9]+\\.com$")
@NotBlank(message = "Email is required. Please provide your email.")
+ @Size(min = 14, max = 72, message = "Email must be between 14 and 72 digits")
String email,
@Size(max = 5000, message = "About user info must be less than 5000 digits")
String about,
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java
index e20ce98..4611492 100644
--- a/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java
+++ b/src/main/java/com/videogamescatalogue/backend/repository/UserRepository.java
@@ -8,6 +8,8 @@
public interface UserRepository extends JpaRepository {
boolean existsByEmail(String email);
+ boolean existsByProfileNameIgnoreCase(String profileName);
+
@EntityGraph(attributePaths = "roles")
Optional findByEmail(String email);
}
diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java
index 5de302c..ae29973 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java
@@ -39,7 +39,8 @@ private void init() {
@Override
public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) {
- checkUserAlreadyExists(requestDto.email());
+ checkUserAlreadyExistsByEmail(requestDto.email());
+ checkProfileNameAlreadyExists(requestDto.profileName());
User user = userMapper.toModel(requestDto);
user.setPassword(passwordEncoder.encode(requestDto.password()));
@@ -105,13 +106,20 @@ private void validateCurrentPassword(
}
}
- private void checkUserAlreadyExists(String email) {
+ private void checkUserAlreadyExistsByEmail(String email) {
if (userRepository.existsByEmail(email)) {
throw new RegistrationException("Can't register user. User with email: "
+ email + " is already registered.");
}
}
+ private void checkProfileNameAlreadyExists(String profileName) {
+ if (userRepository.existsByProfileNameIgnoreCase(profileName)) {
+ throw new RegistrationException("Can't register user with profileName "
+ + profileName + ". This profileName is already in use.");
+ }
+ }
+
private UserResponseDto getUserInfoById(Long userId) {
User user = userRepository.findById(userId).orElseThrow(
() -> new EntityNotFoundException(
From 0788d61cfd5b9e822e0d3bb35938881e9b3c77cd Mon Sep 17 00:00:00 2001
From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com>
Date: Thu, 22 Jan 2026 20:10:51 +0200
Subject: [PATCH 38/41] Add developer info to game (#55)
* created developer entiry, configured liquibase
* added logic to get developers info if missing
* refactored mapping
* added developer info
---
.../dto/external/ApiResponseDeveloperDto.java | 7 +++
.../dto/external/ApiResponseFullGameDto.java | 2 +
.../dto/internal/developer/DeveloperDto.java | 8 ++++
.../backend/dto/internal/game/GameDto.java | 2 +
.../dto/internal/game/GameWithStatusDto.java | 2 +
.../mapper/developer/DeveloperMapper.java | 24 ++++++++++
.../mapper/developer/DeveloperProvider.java | 48 +++++++++++++++++++
.../backend/mapper/game/GameMapper.java | 13 ++++-
.../backend/model/Developer.java | 26 ++++++++++
.../backend/model/Game.java | 9 ++++
.../repository/DeveloperRepository.java | 12 +++++
.../backend/service/game/GameServiceImpl.java | 25 ++++++++--
.../changes/16-create-developers-table.yaml | 33 +++++++++++++
.../17-create-games-developers-table.yaml | 43 +++++++++++++++++
.../db/changelog/db.changelog-master.yaml | 4 ++
.../service/game/GameServiceImplTest.java | 21 ++++++++
.../usergame/UserGameServiceImplTest.java | 9 +++-
17 files changed, 283 insertions(+), 5 deletions(-)
create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseDeveloperDto.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/developer/DeveloperDto.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperMapper.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperProvider.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/model/Developer.java
create mode 100644 src/main/java/com/videogamescatalogue/backend/repository/DeveloperRepository.java
create mode 100644 src/main/resources/db/changelog/changes/16-create-developers-table.yaml
create mode 100644 src/main/resources/db/changelog/changes/17-create-games-developers-table.yaml
diff --git a/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseDeveloperDto.java b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseDeveloperDto.java
new file mode 100644
index 0000000..1b6ec8b
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseDeveloperDto.java
@@ -0,0 +1,7 @@
+package com.videogamescatalogue.backend.dto.external;
+
+public record ApiResponseDeveloperDto(
+ Long id,
+ String name
+) {
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java
index 53741cf..6ed5f6f 100644
--- a/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java
+++ b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java
@@ -15,6 +15,8 @@ public record ApiResponseFullGameDto(
List genres,
+ List developers,
+
BigDecimal rating
) {
}
diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/developer/DeveloperDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/developer/DeveloperDto.java
new file mode 100644
index 0000000..64a1ee1
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/developer/DeveloperDto.java
@@ -0,0 +1,8 @@
+package com.videogamescatalogue.backend.dto.internal.developer;
+
+public record DeveloperDto(
+ Long id,
+ Long apiId,
+ String name
+) {
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java
index 69f8089..1008165 100644
--- a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java
+++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java
@@ -1,5 +1,6 @@
package com.videogamescatalogue.backend.dto.internal.game;
+import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto;
import com.videogamescatalogue.backend.dto.internal.genre.GenreDto;
import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto;
import java.math.BigDecimal;
@@ -12,6 +13,7 @@ public record GameDto(
String backgroundImage,
Set platforms,
Set genres,
+ Set developers,
BigDecimal apiRating,
String description
) {
diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java
index 1df968d..bfa39fc 100644
--- a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java
+++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java
@@ -1,5 +1,6 @@
package com.videogamescatalogue.backend.dto.internal.game;
+import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto;
import com.videogamescatalogue.backend.dto.internal.genre.GenreDto;
import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto;
import com.videogamescatalogue.backend.model.UserGame;
@@ -13,6 +14,7 @@ public record GameWithStatusDto(
String backgroundImage,
Set platforms,
Set genres,
+ Set developers,
BigDecimal apiRating,
String description,
UserGame.GameStatus status
diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperMapper.java
new file mode 100644
index 0000000..28f0e20
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperMapper.java
@@ -0,0 +1,24 @@
+package com.videogamescatalogue.backend.mapper.developer;
+
+import com.videogamescatalogue.backend.config.MapperConfig;
+import com.videogamescatalogue.backend.dto.external.ApiResponseDeveloperDto;
+import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto;
+import com.videogamescatalogue.backend.model.Developer;
+import java.util.List;
+import java.util.Set;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(config = MapperConfig.class)
+public interface DeveloperMapper {
+ @Mapping(source = "id", target = "apiId")
+ @Mapping(target = "id", ignore = true)
+ Developer toModel(ApiResponseDeveloperDto apiResponseDeveloperDto);
+
+ Set toModelSet(List developers);
+
+ Set toDtoSet(Set developers);
+
+ DeveloperDto toDto(Developer developer);
+
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperProvider.java b/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperProvider.java
new file mode 100644
index 0000000..9897f7b
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperProvider.java
@@ -0,0 +1,48 @@
+package com.videogamescatalogue.backend.mapper.developer;
+
+import com.videogamescatalogue.backend.dto.external.ApiResponseDeveloperDto;
+import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto;
+import com.videogamescatalogue.backend.model.Developer;
+import com.videogamescatalogue.backend.repository.DeveloperRepository;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import org.mapstruct.Named;
+import org.springframework.stereotype.Component;
+
+@RequiredArgsConstructor
+@Component
+public class DeveloperProvider {
+ private final DeveloperMapper developerMapper;
+ private final DeveloperRepository developerRepository;
+
+ @Named("toDevelopersSet")
+ public Set toDevelopersSet(List developers) {
+ List developerApiIds = developers.stream()
+ .map(ApiResponseDeveloperDto::id)
+ .toList();
+
+ List existingDevelopers = developerRepository.findAllByApiIdIn(developerApiIds);
+
+ Map existingDevelopersMap = existingDevelopers.stream()
+ .collect(Collectors.toMap(
+ Developer::getApiId,
+ d -> d
+ ));
+
+ Set developersSet = developers.stream()
+ .map(d -> existingDevelopersMap.getOrDefault(
+ d.id(),
+ developerMapper.toModel(d)))
+ .collect(Collectors.toSet());
+
+ return developersSet;
+ }
+
+ @Named("toDeveloperDtosSet")
+ public Set toDeveloperDtosSet(Set developers) {
+ return developerMapper.toDtoSet(developers);
+ }
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java
index 3458fe7..513dc15 100644
--- a/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java
+++ b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java
@@ -6,6 +6,7 @@
import com.videogamescatalogue.backend.dto.internal.game.GameDto;
import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto;
import com.videogamescatalogue.backend.exception.ParsingException;
+import com.videogamescatalogue.backend.mapper.developer.DeveloperProvider;
import com.videogamescatalogue.backend.mapper.genre.GenreProvider;
import com.videogamescatalogue.backend.mapper.platform.PlatformProvider;
import com.videogamescatalogue.backend.model.Game;
@@ -17,7 +18,11 @@
import org.mapstruct.Mapping;
import org.mapstruct.Named;
-@Mapper(config = MapperConfig.class, uses = {PlatformProvider.class, GenreProvider.class})
+@Mapper(config = MapperConfig.class, uses = {
+ PlatformProvider.class,
+ GenreProvider.class,
+ DeveloperProvider.class
+})
public interface GameMapper {
List toModelList(List games);
@@ -34,14 +39,20 @@ public interface GameMapper {
@Mapping(source = "released", target = "year", qualifiedByName = "toYear")
@Mapping(source = "platforms", target = "platforms", qualifiedByName = "toPlatformsSet")
@Mapping(source = "genres", target = "genres", qualifiedByName = "toGenresSet")
+ @Mapping(source = "developers", target = "developers", qualifiedByName = "toDevelopersSet")
@Mapping(source = "rating", target = "apiRating")
Game toModel(ApiResponseFullGameDto apiResponseGameDto);
@Mapping(source = "platforms", target = "platforms", qualifiedByName = "toPlatformDtosSet")
@Mapping(source = "genres", target = "genres", qualifiedByName = "toGenreDtosSet")
+ @Mapping(source = "developers", target = "developers", qualifiedByName = "toDeveloperDtosSet")
GameDto toDto(Game game);
@Mapping(target = "status", source = "status")
+ @Mapping(
+ source = "game.developers", target = "developers",
+ qualifiedByName = "toDeveloperDtosSet"
+ )
GameWithStatusDto toDtoWithStatus(Game game, UserGame.GameStatus status);
@Named("toYear")
diff --git a/src/main/java/com/videogamescatalogue/backend/model/Developer.java b/src/main/java/com/videogamescatalogue/backend/model/Developer.java
new file mode 100644
index 0000000..8af6844
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/model/Developer.java
@@ -0,0 +1,26 @@
+package com.videogamescatalogue.backend.model;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.Setter;
+
+@Entity
+@Table(name = "developers")
+@Getter
+@Setter
+public class Developer {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false, unique = true)
+ private Long apiId;
+
+ @Column(nullable = false)
+ private String name;
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java
index 449fda0..1fd14e8 100644
--- a/src/main/java/com/videogamescatalogue/backend/model/Game.java
+++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java
@@ -57,6 +57,15 @@ public class Game {
)
private Set genres = new HashSet<>();
+ @ManyToMany
+ @JoinTable(
+ name = "games_developers",
+ joinColumns = @JoinColumn(name = "game_id"),
+ inverseJoinColumns = @JoinColumn(name = "developer_id"),
+ uniqueConstraints = @UniqueConstraint(columnNames = {"game_id", "developer_id"})
+ )
+ private Set developers = new HashSet<>();
+
private BigDecimal apiRating;
private String description;
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/DeveloperRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/DeveloperRepository.java
new file mode 100644
index 0000000..fbb8a77
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/repository/DeveloperRepository.java
@@ -0,0 +1,12 @@
+package com.videogamescatalogue.backend.repository;
+
+import com.videogamescatalogue.backend.model.Developer;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface DeveloperRepository extends JpaRepository {
+ Optional findByApiId(Long apiId);
+
+ List findAllByApiIdIn(List apiIds);
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java
index 8671744..8e4f841 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java
@@ -5,18 +5,23 @@
import com.videogamescatalogue.backend.dto.internal.GameSearchParameters;
import com.videogamescatalogue.backend.dto.internal.game.GameDto;
import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto;
+import com.videogamescatalogue.backend.mapper.developer.DeveloperMapper;
import com.videogamescatalogue.backend.mapper.game.GameMapper;
+import com.videogamescatalogue.backend.model.Developer;
import com.videogamescatalogue.backend.model.Game;
import com.videogamescatalogue.backend.model.User;
import com.videogamescatalogue.backend.model.UserGame;
+import com.videogamescatalogue.backend.repository.DeveloperRepository;
import com.videogamescatalogue.backend.repository.GameRepository;
import com.videogamescatalogue.backend.repository.SpecificationBuilder;
import com.videogamescatalogue.backend.repository.UserGameRepository;
import com.videogamescatalogue.backend.service.RawgApiClient;
+import jakarta.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
@@ -32,9 +37,11 @@
public class GameServiceImpl implements GameService {
private final RawgApiClient apiClient;
private final GameMapper gameMapper;
+ private final DeveloperMapper developerMapper;
private final GameRepository gameRepository;
private final SpecificationBuilder specificationBuilder;
private final UserGameRepository userGameRepository;
+ private final DeveloperRepository developerRepository;
@Override
public void fetchBestGames() {
@@ -82,6 +89,7 @@ public Page getAllGamesFromDb(Pageable pageable) {
.map(gameMapper::toDto);
}
+ @Transactional
@Override
public GameWithStatusDto getByApiId(Long apiId, User user) {
Game game = findOrUpdate(apiId);
@@ -154,15 +162,26 @@ private Game findOrUpdate(Long apiId) {
}
Game game = gameOptional.get();
if (game.getDescription() == null) {
- return updateGameDescription(apiId, game);
+ updateGameDescription(apiId, game);
+ }
+ if (game.getDevelopers().isEmpty()) {
+ updateGameDevelopers(apiId, game);
}
return game;
}
- private Game updateGameDescription(Long apiId, Game game) {
+ private void updateGameDescription(Long apiId, Game game) {
ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId);
game.setDescription(apiGame.description());
- return gameRepository.save(game);
+ gameRepository.save(game);
+ }
+
+ private void updateGameDevelopers(Long apiId, Game game) {
+ ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId);
+ Set developers = developerMapper.toModelSet(apiGame.developers());
+ developerRepository.saveAll(developers);
+ game.setDevelopers(developers);
+ gameRepository.save(game);
}
private Game findFromApi(Long apiId) {
diff --git a/src/main/resources/db/changelog/changes/16-create-developers-table.yaml b/src/main/resources/db/changelog/changes/16-create-developers-table.yaml
new file mode 100644
index 0000000..2cb4f89
--- /dev/null
+++ b/src/main/resources/db/changelog/changes/16-create-developers-table.yaml
@@ -0,0 +1,33 @@
+databaseChangeLog:
+ - changeSet:
+ id: create-developers-table
+ author: julia
+ preConditions:
+ - onFail: MARK_RAN
+ - not:
+ - tableExists:
+ tableName: developers
+ changes:
+ - createTable:
+ tableName: developers
+ columns:
+ - column:
+ name: id
+ type: BIGINT
+ autoIncrement: true
+ constraints:
+ primaryKey: true
+ nullable: false
+
+ - column:
+ name: api_id
+ type: BIGINT
+ constraints:
+ nullable: false
+ unique: true
+
+ - column:
+ name: name
+ type: VARCHAR(200)
+ constraints:
+ nullable: false
diff --git a/src/main/resources/db/changelog/changes/17-create-games-developers-table.yaml b/src/main/resources/db/changelog/changes/17-create-games-developers-table.yaml
new file mode 100644
index 0000000..bc69400
--- /dev/null
+++ b/src/main/resources/db/changelog/changes/17-create-games-developers-table.yaml
@@ -0,0 +1,43 @@
+databaseChangeLog:
+ - changeSet:
+ id: create-games-developers-table
+ author: julia
+ preConditions:
+ - onFail: MARK_RAN
+ - not:
+ - tableExists:
+ tableName: games_developers
+ changes:
+ - createTable:
+ tableName: games_developers
+ columns:
+ - column:
+ name: game_id
+ type: BIGINT
+ constraints:
+ nullable: false
+
+ - column:
+ name: developer_id
+ type: BIGINT
+ constraints:
+ nullable: false
+
+ - addPrimaryKey:
+ tableName: games_developers
+ columnNames: game_id, developer_id
+ constraintName: pk_games_developers
+
+ - addForeignKeyConstraint:
+ baseTableName: games_developers
+ baseColumnNames: game_id
+ referencedTableName: games
+ referencedColumnNames: id
+ constraintName: fk_games_developers_game
+
+ - addForeignKeyConstraint:
+ baseTableName: games_developers
+ baseColumnNames: developer_id
+ referencedTableName: developers
+ referencedColumnNames: id
+ constraintName: fk_games_developers_developer
diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml
index 0763141..6baf17b 100644
--- a/src/main/resources/db/changelog/db.changelog-master.yaml
+++ b/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -29,3 +29,7 @@ databaseChangeLog:
file: classpath:/db/changelog/changes/14-create-user-game-table.yaml
- include:
file: classpath:/db/changelog/changes/15-create-comments-table.yaml
+ - include:
+ file: classpath:/db/changelog/changes/16-create-developers-table.yaml
+ - include:
+ file: classpath:/db/changelog/changes/17-create-games-developers-table.yaml
diff --git a/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java
index 850368b..7f4e014 100644
--- a/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java
+++ b/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java
@@ -9,15 +9,18 @@
import static org.mockito.Mockito.when;
import com.videogamescatalogue.backend.dto.external.ApiPlatformWrapper;
+import com.videogamescatalogue.backend.dto.external.ApiResponseDeveloperDto;
import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto;
import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto;
import com.videogamescatalogue.backend.dto.external.ApiResponseGenreDto;
import com.videogamescatalogue.backend.dto.external.ApiResponsePlatformDto;
+import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto;
import com.videogamescatalogue.backend.dto.internal.game.GameDto;
import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto;
import com.videogamescatalogue.backend.dto.internal.genre.GenreDto;
import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto;
import com.videogamescatalogue.backend.mapper.game.GameMapper;
+import com.videogamescatalogue.backend.model.Developer;
import com.videogamescatalogue.backend.model.Game;
import com.videogamescatalogue.backend.model.Genre;
import com.videogamescatalogue.backend.model.Platform;
@@ -84,6 +87,11 @@ void setUp() {
genreModel.setId(20L);
genreModel.setName(Genre.Name.ACTION);
+ Developer developer = new Developer();
+ developer.setId(30L);
+ developer.setApiId(457389L);
+ developer.setName("Developer");
+
gameModel = new Game();
gameModel.setApiId(1L);
gameModel.setName("Game");
@@ -93,6 +101,8 @@ void setUp() {
gameModel.setGenres(Set.of(genreModel));
gameModel.setApiRating(BigDecimal.valueOf(4.75));
gameModel.setDescription(null);
+ gameModel.setDevelopers(Set.of(developer));
+
gameModelList = List.of(gameModel);
responseGamesIds = List.of(1L);
zeroGamesToSave = List.of();
@@ -102,24 +112,35 @@ void setUp() {
PlatformDto platformDto = new PlatformDto("PC");
GenreDto genreDto = new GenreDto("Action");
+ DeveloperDto developerDto = new DeveloperDto(
+ 30L, 457389L, "Developer"
+ );
gameDto = new GameDto(
1L, "Game", 2025, "link",
Set.of(platformDto), Set.of(genreDto),
+ Set.of(developerDto),
BigDecimal.valueOf(4.75), null
);
oneGameDtoPage = new PageImpl<>(List.of(gameDto));
gameWithDescrAndNullStatusDto = new GameWithStatusDto(
1L, "Game", 2025, "link",
Set.of(platformDto), Set.of(genreDto),
+ Set.of(developerDto),
BigDecimal.valueOf(4.75),
"description", null
);
+ ApiResponseDeveloperDto apiResponseDeveloperDto =
+ new ApiResponseDeveloperDto(
+ 457389L, "Developer"
+ );
+
apiResponseFullGameDto = new ApiResponseFullGameDto(
1L, "Game", "description",
"2025-12-12", "link",
List.of(apiPlatformWrapper),
List.of(apiResponseGenreDto),
+ List.of(apiResponseDeveloperDto),
BigDecimal.valueOf(4.75)
);
diff --git a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java
index 441d9d7..25000c1 100644
--- a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java
+++ b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java
@@ -8,6 +8,7 @@
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
+import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto;
import com.videogamescatalogue.backend.dto.internal.game.GameDto;
import com.videogamescatalogue.backend.dto.internal.genre.GenreDto;
import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto;
@@ -45,9 +46,10 @@ class UserGameServiceImplTest {
private User user;
private UserGame userGame;
private UserGameDto userGameDto;
- private GameDto gameDto;
private PlatformDto platformDto;
private GenreDto genreDto;
+ private DeveloperDto developerDto;
+ private GameDto gameDto;
@BeforeEach
void setUp() {
@@ -79,11 +81,16 @@ void setUp() {
genreDto = new GenreDto("Action");
+ developerDto = new DeveloperDto(
+ 30L, 23456L, "Developer"
+ );
+
gameDto = new GameDto(
1234L, "Game name",
2016, "link",
Set.of(platformDto),
Set.of(genreDto),
+ Set.of(developerDto),
BigDecimal.valueOf(4.8),
"description"
);
From d87a8f39202a6748c2bd9ea3d5909f86c04f8ae9 Mon Sep 17 00:00:00 2001
From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com>
Date: Thu, 22 Jan 2026 20:47:07 +0200
Subject: [PATCH 39/41] removed email from update method (#56)
---
.../backend/dto/internal/user/UpdateUserRequestDto.java | 3 ---
.../backend/service/user/UserServiceImplTest.java | 2 +-
2 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UpdateUserRequestDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UpdateUserRequestDto.java
index 90bdc95..29c0db2 100644
--- a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UpdateUserRequestDto.java
+++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UpdateUserRequestDto.java
@@ -1,12 +1,9 @@
package com.videogamescatalogue.backend.dto.internal.user;
-import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;
public record UpdateUserRequestDto(
String profileName,
- @Email(message = "Invalid format of email.")
- String email,
@Size(max = 5000, message = "About user info must be less than 5000 digits")
String about,
@Size(max = 50, message = "Location info must be less than 50 digits")
diff --git a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java
index 8d7ef53..d5064f0 100644
--- a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java
+++ b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java
@@ -100,7 +100,7 @@ void setUp() {
updateUserRequestDto = new UpdateUserRequestDto(
"updated profileName", null,
- null, null
+ null
);
changePasswordRequestDto = new ChangePasswordRequestDto(
From 93715e93ccb97420f1ab0db8a31870a1260d3713 Mon Sep 17 00:00:00 2001
From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com>
Date: Thu, 22 Jan 2026 21:44:55 +0200
Subject: [PATCH 40/41] return token after registartion (#57)
---
.../controller/AuthenticationController.java | 3 ++-
.../user/UserRegistrationResponseDto.java | 10 +++++++++
.../backend/mapper/user/UserMapper.java | 4 ++++
.../backend/service/user/UserService.java | 3 ++-
.../backend/service/user/UserServiceImpl.java | 16 ++++++++++++--
.../service/user/UserServiceImplTest.java | 21 ++++++++++++++++---
6 files changed, 50 insertions(+), 7 deletions(-)
create mode 100644 src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationResponseDto.java
diff --git a/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java b/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java
index f4f7c3f..b619977 100644
--- a/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java
+++ b/src/main/java/com/videogamescatalogue/backend/controller/AuthenticationController.java
@@ -3,6 +3,7 @@
import com.videogamescatalogue.backend.dto.internal.user.UserLoginRequestDto;
import com.videogamescatalogue.backend.dto.internal.user.UserLoginResponseDto;
import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto;
+import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto;
import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto;
import com.videogamescatalogue.backend.security.AuthenticationService;
import com.videogamescatalogue.backend.service.user.UserService;
@@ -45,7 +46,7 @@ public class AuthenticationController {
}
)
@PostMapping("/registration")
- UserResponseDto registerUser(
+ UserRegistrationResponseDto registerUser(
@RequestBody @Valid UserRegistrationRequestDto requestDto
) {
return userService.registerUser(requestDto);
diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationResponseDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationResponseDto.java
new file mode 100644
index 0000000..4e04a2c
--- /dev/null
+++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/user/UserRegistrationResponseDto.java
@@ -0,0 +1,10 @@
+package com.videogamescatalogue.backend.dto.internal.user;
+
+public record UserRegistrationResponseDto(
+ Long id,
+ String profileName,
+ String about,
+ String location,
+ String token
+) {
+}
diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java
index 60a75e0..e93d4ee 100644
--- a/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java
+++ b/src/main/java/com/videogamescatalogue/backend/mapper/user/UserMapper.java
@@ -3,6 +3,7 @@
import com.videogamescatalogue.backend.config.MapperConfig;
import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto;
import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto;
+import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto;
import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto;
import com.videogamescatalogue.backend.model.User;
import org.mapstruct.Mapper;
@@ -17,5 +18,8 @@ public interface UserMapper {
@Mapping(source = "id", target = "userGames", qualifiedByName = "getStatusDtoList")
UserResponseDto toDto(User user);
+ @Mapping(source = "token", target = "token")
+ UserRegistrationResponseDto toRegistrationResponseDto(User user, String token);
+
User updateProfileInfo(@MappingTarget User user, UpdateUserRequestDto requestDto);
}
diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java
index 73c69ac..5212084 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserService.java
@@ -3,11 +3,12 @@
import com.videogamescatalogue.backend.dto.internal.user.ChangePasswordRequestDto;
import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto;
import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto;
+import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto;
import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto;
import com.videogamescatalogue.backend.model.User;
public interface UserService {
- UserResponseDto registerUser(
+ UserRegistrationResponseDto registerUser(
UserRegistrationRequestDto requestDto
);
diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java
index ae29973..cea8ec2 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java
@@ -2,7 +2,10 @@
import com.videogamescatalogue.backend.dto.internal.user.ChangePasswordRequestDto;
import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto;
+import com.videogamescatalogue.backend.dto.internal.user.UserLoginRequestDto;
+import com.videogamescatalogue.backend.dto.internal.user.UserLoginResponseDto;
import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto;
+import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto;
import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto;
import com.videogamescatalogue.backend.exception.AuthenticationRequiredException;
import com.videogamescatalogue.backend.exception.EntityNotFoundException;
@@ -13,7 +16,9 @@
import com.videogamescatalogue.backend.model.User;
import com.videogamescatalogue.backend.repository.RoleRepository;
import com.videogamescatalogue.backend.repository.UserRepository;
+import com.videogamescatalogue.backend.security.AuthenticationService;
import jakarta.annotation.PostConstruct;
+import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.crypto.password.PasswordEncoder;
@@ -27,6 +32,7 @@ public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final RoleRepository roleRepository;
+ private final AuthenticationService authenticationService;
private Role roleUser;
@PostConstruct
@@ -37,8 +43,9 @@ private void init() {
);
}
+ @Transactional
@Override
- public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) {
+ public UserRegistrationResponseDto registerUser(UserRegistrationRequestDto requestDto) {
checkUserAlreadyExistsByEmail(requestDto.email());
checkProfileNameAlreadyExists(requestDto.profileName());
@@ -50,7 +57,12 @@ public UserResponseDto registerUser(UserRegistrationRequestDto requestDto) {
log.info("User registered successfully, id={}",savedUser.getId());
- return userMapper.toDto(savedUser);
+ UserLoginResponseDto authResponseDto = authenticationService.authenticate(
+ new UserLoginRequestDto(
+ requestDto.email(), requestDto.password()
+ ));
+
+ return userMapper.toRegistrationResponseDto(savedUser, authResponseDto.token());
}
@Override
diff --git a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java
index d5064f0..d689459 100644
--- a/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java
+++ b/src/test/java/com/videogamescatalogue/backend/service/user/UserServiceImplTest.java
@@ -11,7 +11,10 @@
import com.videogamescatalogue.backend.dto.internal.user.ChangePasswordRequestDto;
import com.videogamescatalogue.backend.dto.internal.user.UpdateUserRequestDto;
+import com.videogamescatalogue.backend.dto.internal.user.UserLoginRequestDto;
+import com.videogamescatalogue.backend.dto.internal.user.UserLoginResponseDto;
import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto;
+import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto;
import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto;
import com.videogamescatalogue.backend.dto.internal.usergame.UserGameStatusDto;
import com.videogamescatalogue.backend.exception.AuthenticationRequiredException;
@@ -21,6 +24,7 @@
import com.videogamescatalogue.backend.model.Role;
import com.videogamescatalogue.backend.model.User;
import com.videogamescatalogue.backend.repository.UserRepository;
+import com.videogamescatalogue.backend.security.AuthenticationService;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
@@ -39,6 +43,8 @@ class UserServiceImplTest {
private UserMapper userMapper;
@Mock
private PasswordEncoder passwordEncoder;
+ @Mock
+ private AuthenticationService authenticationService;
@InjectMocks
private UserServiceImpl userService;
private String email = "test@gmail.com";
@@ -128,6 +134,11 @@ void registerUser_userExists_throwException() {
@Test
void registerUser_validRequest_returnUserDto() {
+ final UserRegistrationResponseDto responseDto = new UserRegistrationResponseDto(
+ 1L, "profileName", "about",
+ "location", "token"
+ );
+
String encodedPassword = user.getPassword();
when(userRepository.existsByEmail(email))
@@ -139,13 +150,17 @@ void registerUser_validRequest_returnUserDto() {
.thenReturn(encodedPassword);
when(userRepository.save(user))
.thenReturn(user);
- when(userMapper.toDto(user)).thenReturn(responseDtoBob);
+ when(authenticationService.authenticate(new UserLoginRequestDto(
+ registrationRequestDto.email(), registrationRequestDto.password()
+ ))).thenReturn(new UserLoginResponseDto("token", 10L));
+ when(userMapper.toRegistrationResponseDto(user, "token"))
+ .thenReturn(responseDto);
- UserResponseDto actual = userService.registerUser(
+ UserRegistrationResponseDto actual = userService.registerUser(
registrationRequestDto
);
- assertEquals(responseDtoBob, actual);
+ assertEquals(responseDto, actual);
verify(passwordEncoder).encode(registrationRequestDto.password());
verify(userRepository).existsByEmail(email);
From dc703c88ff69c8db4dcbf0a1f75bc609bf5d32a2 Mon Sep 17 00:00:00 2001
From: Yuliia Tatarchuk <153382678+YuliiaNisha@users.noreply.github.com>
Date: Wed, 28 Jan 2026 15:17:21 +0200
Subject: [PATCH 41/41] corrected duplicate developers save (#59)
* corrected duplicate developers save
* ran mvn clean package
---
.../backend/controller/GameController.java | 32 ---------
.../backend/model/Game.java | 4 ++
.../backend/repository/GameRepository.java | 7 ++
.../backend/security/JwtUtil.java | 14 ++--
.../backend/service/RawgApiClient.java | 13 ++--
.../service/comment/CommentServiceImpl.java | 12 ++--
.../backend/service/game/GameServiceImpl.java | 67 +++++++++++++------
.../backend/service/user/UserServiceImpl.java | 18 ++++-
8 files changed, 96 insertions(+), 71 deletions(-)
diff --git a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java
index ef62b38..bcbcd65 100644
--- a/src/main/java/com/videogamescatalogue/backend/controller/GameController.java
+++ b/src/main/java/com/videogamescatalogue/backend/controller/GameController.java
@@ -3,8 +3,6 @@
import com.videogamescatalogue.backend.dto.internal.GameSearchParameters;
import com.videogamescatalogue.backend.dto.internal.game.GameDto;
import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto;
-import com.videogamescatalogue.backend.model.Genre;
-import com.videogamescatalogue.backend.model.Platform;
import com.videogamescatalogue.backend.model.User;
import com.videogamescatalogue.backend.service.game.GameService;
import io.swagger.v3.oas.annotations.Operation;
@@ -12,9 +10,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
-import java.util.Arrays;
import java.util.Map;
-import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -130,7 +126,6 @@ public Page search(
@PageableDefault(size = DEFAULT_PAGE_SIZE)
Pageable pageable
) {
- validateSearchParams(searchParameters);
return gameService.search(searchParameters, pageable);
}
@@ -149,31 +144,4 @@ public Page apiSearch(
) {
return gameService.apiSearch(searchParams);
}
-
- private void validateSearchParams(GameSearchParameters searchParameters) {
- if (searchParameters.platforms() != null) {
- try {
- searchParameters.platforms()
- .forEach(
- p -> Platform.GeneralName.valueOf(p.toUpperCase())
- );
- } catch (IllegalArgumentException e) {
- throw new IllegalArgumentException(
- "Invalid platforms provided. Valid platforms: "
- + Arrays.stream(Platform.GeneralName.values())
- .map(Enum::toString)
- .collect(Collectors.joining(", ")), e);
- }
- }
- if (searchParameters.genres() != null) {
- try {
- searchParameters.genres().forEach(g -> Genre.Name.valueOf(g.toUpperCase()));
- } catch (IllegalArgumentException e) {
- throw new IllegalArgumentException("Invalid genres provided. Valid genres: "
- + Arrays.stream(Genre.Name.values())
- .map(Enum::toString)
- .collect(Collectors.joining(", ")), e);
- }
- }
- }
}
diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java
index 1fd14e8..010f878 100644
--- a/src/main/java/com/videogamescatalogue/backend/model/Game.java
+++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java
@@ -13,6 +13,7 @@
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.Set;
+import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.SQLDelete;
@@ -39,6 +40,7 @@ public class Game {
private String backgroundImage;
+ @EqualsAndHashCode.Exclude
@ManyToMany
@JoinTable(
name = "games_platforms",
@@ -48,6 +50,7 @@ public class Game {
)
private Set platforms = new HashSet<>();
+ @EqualsAndHashCode.Exclude
@ManyToMany
@JoinTable(
name = "games_genres",
@@ -57,6 +60,7 @@ public class Game {
)
private Set genres = new HashSet<>();
+ @EqualsAndHashCode.Exclude
@ManyToMany
@JoinTable(
name = "games_developers",
diff --git a/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java
index cf88999..c11a615 100644
--- a/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java
+++ b/src/main/java/com/videogamescatalogue/backend/repository/GameRepository.java
@@ -3,11 +3,18 @@
import com.videogamescatalogue.backend.model.Game;
import java.util.List;
import java.util.Optional;
+import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface GameRepository extends JpaRepository, JpaSpecificationExecutor {
+ @EntityGraph(attributePaths = {
+ "platforms", "genres", "developers"
+ })
Optional findByApiId(Long apiId);
+ @EntityGraph(attributePaths = {
+ "platforms", "genres", "developers"
+ })
List findAllByApiIdIn(List apiIds);
}
diff --git a/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java b/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java
index 87d254f..a51e9e5 100644
--- a/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java
+++ b/src/main/java/com/videogamescatalogue/backend/security/JwtUtil.java
@@ -41,13 +41,6 @@ public boolean isValidToken(String token) {
}
}
- private Jws getAllClaims(String token) {
- return Jwts.parserBuilder()
- .setSigningKey(secret)
- .build()
- .parseClaimsJws(token);
- }
-
public String getUsername(String token) {
return getSpecificClaim(token, Claims::getSubject);
}
@@ -56,4 +49,11 @@ private T getSpecificClaim(String token, Function claimsResolver)
Jws allClaims = getAllClaims(token);
return claimsResolver.apply(allClaims.getBody());
}
+
+ private Jws getAllClaims(String token) {
+ return Jwts.parserBuilder()
+ .setSigningKey(secret)
+ .build()
+ .parseClaimsJws(token);
+ }
}
diff --git a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java
index d22dacb..d47504e 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/RawgApiClient.java
@@ -45,6 +45,9 @@ public class RawgApiClient {
private static final String DATES_BETWEEN_URL_PART = "&dates=" + LocalDate.now()
+ "%2C" + LocalDate.now().minusMonths(12);
private static final String METACRITIC_URL_PART = "metacritic=80%2C100";
+ private static final int NUMBER_OF_DOWNLOAD_PORTIONS = 11;
+ private static final String REQUEST_HEADER_NAME = "User-Agent";
+ private static final String REQUEST_HEADER_VALUE = "VideoGamesCatalogue";
@Value("${rawg.key}")
private String apiKey;
@@ -56,7 +59,7 @@ public List getBestGames() {
ArrayList result = new ArrayList<>();
- for (int i = 1; i < 11; i++) {
+ for (int i = 1; i < NUMBER_OF_DOWNLOAD_PORTIONS; i++) {
log.info("Create request for page {}", i);
String url = BASE_URL + GAME_URL_PART
@@ -70,7 +73,7 @@ public List getBestGames() {
HttpRequest httpRequest = HttpRequest.newBuilder()
.GET()
.uri(URI.create(url))
- .header("User-Agent", "VideoGamesCatalogue")
+ .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE)
.build();
ApiResponseGames responseObject = getResponseGamesList(httpRequest);
@@ -95,7 +98,7 @@ public Page getAllGames(Pageable pageable) {
HttpRequest httpRequest = HttpRequest.newBuilder()
.GET()
.uri(URI.create(url))
- .header("User-Agent", "VideoGamesCatalogue")
+ .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE)
.build();
ApiResponseGames responseObject = getResponseGamesList(httpRequest);
@@ -112,7 +115,7 @@ public ApiResponseFullGameDto getGameById(Long id) {
HttpRequest httpRequest = HttpRequest.newBuilder()
.GET()
.uri(URI.create(url))
- .header("User-Agent", "VideoGamesCatalogue")
+ .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE)
.build();
ApiResponseFullGameDto game = getIndividualGame(httpRequest);
@@ -129,7 +132,7 @@ public Page search(Map searchParams) {
HttpRequest httpRequest = HttpRequest.newBuilder()
.GET()
.uri(URI.create(url.toString()))
- .header("User-Agent", "VideoGamesCatalogue")
+ .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE)
.build();
ApiResponseGames responseObject = getResponseGamesList(httpRequest);
diff --git a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java
index 8c15d2d..841e9f1 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/comment/CommentServiceImpl.java
@@ -21,6 +21,7 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
@@ -31,6 +32,7 @@ public class CommentServiceImpl implements CommentService {
private final GameMapper gameMapper;
private final CommentRepository commentRepository;
+ @Transactional
@Override
public CommentDto create(Long gameApiId, CreateCommentRequestDto requestDto, User user) {
Comment comment = commentMapper.toModel(requestDto);
@@ -61,11 +63,6 @@ public Page getUserComments(
return findCommentsByUserId(userId, pageable);
}
- private Page findCommentsByUserId(Long userId, Pageable pageable) {
- Page userComments = commentRepository.findAllByUserId(userId, pageable);
- return userComments.map(commentMapper::toDto);
- }
-
@Override
public CommentDto update(Long commentId, UpdateCommentRequestDto requestDto, Long userId) {
Comment comment = commentRepository.findById(commentId)
@@ -94,6 +91,11 @@ public void delete(Long commentId, Long userId) {
commentRepository.deleteById(commentId);
}
+ private Page findCommentsByUserId(Long userId, Pageable pageable) {
+ Page userComments = commentRepository.findAllByUserId(userId, pageable);
+ return userComments.map(commentMapper::toDto);
+ }
+
private void existsByIdAndUserId(Long commentId, Long userId) {
if (!commentRepository.existsByIdAndUserId(commentId, userId)) {
throw new AccessNotAllowedException("User with id: " + userId
diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java
index 8e4f841..8aa5eb3 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java
@@ -6,9 +6,12 @@
import com.videogamescatalogue.backend.dto.internal.game.GameDto;
import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto;
import com.videogamescatalogue.backend.mapper.developer.DeveloperMapper;
+import com.videogamescatalogue.backend.mapper.developer.DeveloperProvider;
import com.videogamescatalogue.backend.mapper.game.GameMapper;
import com.videogamescatalogue.backend.model.Developer;
import com.videogamescatalogue.backend.model.Game;
+import com.videogamescatalogue.backend.model.Genre;
+import com.videogamescatalogue.backend.model.Platform;
import com.videogamescatalogue.backend.model.User;
import com.videogamescatalogue.backend.model.UserGame;
import com.videogamescatalogue.backend.repository.DeveloperRepository;
@@ -18,6 +21,7 @@
import com.videogamescatalogue.backend.service.RawgApiClient;
import jakarta.transaction.Transactional;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -42,6 +46,7 @@ public class GameServiceImpl implements GameService {
private final SpecificationBuilder specificationBuilder;
private final UserGameRepository userGameRepository;
private final DeveloperRepository developerRepository;
+ private final DeveloperProvider developerProvider;
@Override
public void fetchBestGames() {
@@ -108,6 +113,7 @@ public Page getAllGamesFromApi(Pageable pageable) {
@Override
public Page search(GameSearchParameters searchParameters, Pageable pageable) {
+ validateSearchParams(searchParameters);
Specification specification = specificationBuilder.build(searchParameters);
return gameRepository.findAll(specification, pageable)
@@ -157,35 +163,56 @@ private UserGame.GameStatus getGameStatus(Long apiId, User user) {
private Game findOrUpdate(Long apiId) {
Optional gameOptional = gameRepository.findByApiId(apiId);
+ if (gameOptional.isPresent()
+ && gameOptional.get().getDescription() != null
+ && !gameOptional.get().getDevelopers().isEmpty()) {
+ return gameOptional.get();
+ }
+ ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId);
if (gameOptional.isEmpty()) {
- return findFromApi(apiId);
+ return gameMapper.toModel(apiGame);
}
Game game = gameOptional.get();
+ updateGameInfo(game, apiGame);
+ return game;
+ }
+
+ private void updateGameInfo(Game game, ApiResponseFullGameDto apiGame) {
if (game.getDescription() == null) {
- updateGameDescription(apiId, game);
+ game.setDescription(apiGame.description());
}
if (game.getDevelopers().isEmpty()) {
- updateGameDevelopers(apiId, game);
+ Set developersSet = developerProvider.toDevelopersSet(apiGame.developers());
+ developerRepository.saveAll(developersSet);
+ game.setDevelopers(developersSet);
}
- return game;
- }
-
- private void updateGameDescription(Long apiId, Game game) {
- ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId);
- game.setDescription(apiGame.description());
gameRepository.save(game);
}
- private void updateGameDevelopers(Long apiId, Game game) {
- ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId);
- Set developers = developerMapper.toModelSet(apiGame.developers());
- developerRepository.saveAll(developers);
- game.setDevelopers(developers);
- gameRepository.save(game);
- }
-
- private Game findFromApi(Long apiId) {
- ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId);
- return gameMapper.toModel(apiGame);
+ private void validateSearchParams(GameSearchParameters searchParameters) {
+ if (searchParameters.platforms() != null) {
+ try {
+ searchParameters.platforms()
+ .forEach(
+ p -> Platform.GeneralName.valueOf(p.toUpperCase())
+ );
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ "Invalid platforms provided. Valid platforms: "
+ + Arrays.stream(Platform.GeneralName.values())
+ .map(Enum::toString)
+ .collect(Collectors.joining(", ")), e);
+ }
+ }
+ if (searchParameters.genres() != null) {
+ try {
+ searchParameters.genres().forEach(g -> Genre.Name.valueOf(g.toUpperCase()));
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid genres provided. Valid genres: "
+ + Arrays.stream(Genre.Name.values())
+ .map(Enum::toString)
+ .collect(Collectors.joining(", ")), e);
+ }
+ }
}
}
diff --git a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java
index cea8ec2..eb7a181 100644
--- a/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java
+++ b/src/main/java/com/videogamescatalogue/backend/service/user/UserServiceImpl.java
@@ -7,6 +7,7 @@
import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationRequestDto;
import com.videogamescatalogue.backend.dto.internal.user.UserRegistrationResponseDto;
import com.videogamescatalogue.backend.dto.internal.user.UserResponseDto;
+import com.videogamescatalogue.backend.exception.AccessNotAllowedException;
import com.videogamescatalogue.backend.exception.AuthenticationRequiredException;
import com.videogamescatalogue.backend.exception.EntityNotFoundException;
import com.videogamescatalogue.backend.exception.InvalidInputException;
@@ -78,6 +79,14 @@ public UserResponseDto getUserInfo(Long userId, User authenticatedUser) {
@Override
public UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User authenticatedUser) {
+ if (authenticatedUser == null) {
+ throw new AccessNotAllowedException(
+ "You are not allowed to modify this user info. Please log in."
+ );
+ }
+ if (requestDto.profileName() != null) {
+ checkProfileNameAlreadyExists(requestDto.profileName());
+ }
User updatedUser = userMapper.updateProfileInfo(authenticatedUser, requestDto);
User savedUser = userRepository.save(updatedUser);
@@ -90,6 +99,11 @@ public UserResponseDto updateUserInfo(UpdateUserRequestDto requestDto, User auth
public UserResponseDto changePassword(
ChangePasswordRequestDto requestDto, User authenticatedUser
) {
+ if (authenticatedUser == null) {
+ throw new AccessNotAllowedException(
+ "You are not allowed to modify this user info. Please log in."
+ );
+ }
validateCurrentPassword(requestDto, authenticatedUser);
checkPasswordsMatch(requestDto);
@@ -127,8 +141,8 @@ private void checkUserAlreadyExistsByEmail(String email) {
private void checkProfileNameAlreadyExists(String profileName) {
if (userRepository.existsByProfileNameIgnoreCase(profileName)) {
- throw new RegistrationException("Can't register user with profileName "
- + profileName + ". This profileName is already in use.");
+ throw new RegistrationException("Profile Name "
+ + profileName + " is already in use.");
}
}