[Feature]: Spieler Listen Endpunkt #6

Merged
SZUT-Mehdi merged 1 commit from TD-34-spieler-listen-endpunkt into trunk 2025-03-05 09:39:18 +00:00
8 changed files with 274 additions and 7 deletions

View file

@ -86,6 +86,44 @@ components:
type: string type: string
required: required:
- username - username
#############################################
SZUT-Mehdi marked this conversation as resolved Outdated

Keine Default Wete

Keine Default Wete
# AdministratablePlayer #
#############################################
AdministratablePlayer:
description: a Player
type: object
properties:
username:
type: string
SZUT-Mehdi marked this conversation as resolved Outdated

page, pagesize, order sollten pflicht felder sein.

page, pagesize, order sollten pflicht felder sein.
required:
- username
#############################################
# PlayerFilter #
#############################################
PlayerFilter:
description: Configuration data for query for the getting all players endpoint
type: object
properties:
page:
type: integer
description: "Page number (zero-based index)."
pageSize:
type: integer
description: "Number of players per page."
sortBy:
type: string
description: "Field to sort by (default is username)."
order:
type: string
enum: [ ascending, descending ]
description: "Sorting order (asc or desc)."
username:
type: string
description: "Filter players by username (case-insensitive, partial match)."
required:
- page
- pageSize
- order
responses: responses:
201PlayerCreated: 201PlayerCreated:
description: "201 - Player Created" description: "201 - Player Created"
@ -200,3 +238,30 @@ paths:
$ref: "#/components/responses/500InternalError" $ref: "#/components/responses/500InternalError"
503: 503:
$ref: "#/components/responses/503ServiceUnavailable" $ref: "#/components/responses/503ServiceUnavailable"
/admin/players:
get:
operationId: "GetAllPlayers"
tags:
- admin
summary: "Retrieve a paginated list of players"
description: "Returns a paginat#ed list of players, optionally filtered by username."
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/PlayerFilter"
responses:
200:
description: "A List of all Player"
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/AdministratablePlayer"
401:
$ref: "#/components/responses/401Unauthorized"
500:
$ref: "#/components/responses/500InternalError"
503:
$ref: "#/components/responses/503ServiceUnavailable"

View file

@ -1,16 +1,26 @@
package de.towerdefence.server.admin; package de.towerdefence.server.admin;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import de.towerdefence.server.auth.UserSession; import de.towerdefence.server.auth.UserSession;
import de.towerdefence.server.oas.AdminApi; import de.towerdefence.server.oas.AdminApi;
import de.towerdefence.server.oas.models.AdminAuthInfo; import de.towerdefence.server.oas.models.AdminAuthInfo;
import de.towerdefence.server.oas.models.AdministratablePlayer;
import de.towerdefence.server.oas.models.PlayerFilter;
import de.towerdefence.server.player.Player;
import de.towerdefence.server.player.PlayerRepository;
import de.towerdefence.server.utils.OrderToDirectionMapperService;
import de.towerdefence.server.utils.PlayerMapperService;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
import java.util.Optional; import java.util.Optional;
@Controller @Controller
@ -20,6 +30,16 @@ public class AdminApiController implements AdminApi {
@Autowired @Autowired
UserSession userSession; UserSession userSession;
@Autowired
PlayerRepository playerRepository;
@Autowired
PlayerMapperService playerMapperService;
@Autowired
OrderToDirectionMapperService orderToDirectionMapperService;
@Override @Override
public Optional<ObjectMapper> getObjectMapper() { public Optional<ObjectMapper> getObjectMapper() {
return Optional.empty(); return Optional.empty();
@ -36,4 +56,31 @@ public class AdminApiController implements AdminApi {
authInfo.setUsername(this.userSession.getUsername()); authInfo.setUsername(this.userSession.getUsername());
SZUT-Mehdi marked this conversation as resolved Outdated

Bitte einmal Formatieren

Bitte einmal Formatieren
return ResponseEntity.ok(authInfo); return ResponseEntity.ok(authInfo);
} }
SZUT-Mehdi marked this conversation as resolved Outdated

Wir nutzen das var Keyword nicht, weil es seine Unsicherheiten hat

Wir nutzen das `var` Keyword nicht, weil es seine Unsicherheiten hat
@Override
public ResponseEntity<List<AdministratablePlayer>> getAllPlayers(PlayerFilter body) {
PlayerFilter.OrderEnum order = body.getOrder();
SZUT-Dorian marked this conversation as resolved Outdated

Bitte keine Tenaries nutzen, die machen Code unleserlicher, indem sie Logik hinter magic verstecken

Bitte keine Tenaries nutzen, die machen Code unleserlicher, indem sie Logik hinter magic verstecken
SZUT-Mehdi marked this conversation as resolved Outdated

Keine Tenaries nutzen, sind zu viel Magic und machen Code unleserlich

Keine Tenaries nutzen, sind zu viel Magic und machen Code unleserlich
Integer page = body.getPage();
Integer pageSize = body.getPageSize();
SZUT-Mehdi marked this conversation as resolved Outdated

Keine Default Werte für die Order.
Sollte required sein in der API.

Das Frontend wird ja auch einen Default anzeigen und somit setzten.

Es ist also aufgabe des Frontends dieses Festzulegen und nicht des Servers

Keine Default Werte für die Order. Sollte required sein in der API. Das Frontend wird ja auch einen Default anzeigen und somit setzten. Es ist also aufgabe des Frontends dieses Festzulegen und nicht des Servers
String sortBy = body.getSortBy();
String username = body.getUsername();
SZUT-Mehdi marked this conversation as resolved Outdated

Keine Tenaries nutzen, sind zu viel Magic und machen Code unleserlich

Keine Tenaries nutzen, sind zu viel Magic und machen Code unleserlich
Sort.Direction direction = orderToDirectionMapperService.orderToDirection(order);
SZUT-Mehdi marked this conversation as resolved Outdated

Für dieses mapping, bitte einen Mapper anlegen

Für dieses mapping, bitte einen Mapper anlegen
Pageable pageable = PageRequest.of(page, pageSize, Sort.by(direction, sortBy));
Page<Player> playerPage;
if (username != null && !username.isEmpty()) {
playerPage = playerRepository.findByUsernameContainingIgnoreCase(username, pageable);
} else {
playerPage = playerRepository.findAll(pageable);
}
SZUT-Mehdi marked this conversation as resolved Outdated

Dieser Endpunkt ist immer Paged, deswegen wird dieses If nicht benötigt und die API Spec sollte es auch wieder spiegeln. (Siehe andere Nachricht)

Dieser Endpunkt ist immer Paged, deswegen wird dieses If nicht benötigt und die API Spec sollte es auch wieder spiegeln. (Siehe andere Nachricht)
List<AdministratablePlayer> playersMapped =
playerMapperService.mapPlayersToAdministratablePlayers(playerPage.getContent());
return ResponseEntity.ok(playersMapped);
}
} }

View file

@ -1,8 +1,11 @@
package de.towerdefence.server.player; package de.towerdefence.server.player;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
public interface PlayerRepository extends JpaRepository<Player, Long> { public interface PlayerRepository extends JpaRepository<Player, Long> {
Player findByUsername(String username); Player findByUsername(String username);
boolean existsByUsername(String username); boolean existsByUsername(String username);
Page<Player> findByUsernameContainingIgnoreCase(String username, Pageable pageable);
} }

View file

@ -0,0 +1,18 @@
package de.towerdefence.server.utils;
import de.towerdefence.server.oas.models.PlayerFilter;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
@Component
public class OrderToDirectionMapperService {
public Sort.Direction orderToDirection(PlayerFilter.OrderEnum order) {
if (order == PlayerFilter.OrderEnum.ASCENDING) {
return Sort.Direction.ASC;
} else {
return Sort.Direction.DESC;
}
}
}

View file

@ -0,0 +1,28 @@
package de.towerdefence.server.utils;
import de.towerdefence.server.oas.models.AdministratablePlayer;
import de.towerdefence.server.player.Player;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class PlayerMapperService {
public List<AdministratablePlayer> mapPlayersToAdministratablePlayers(List<Player> players) {
SZUT-Mehdi marked this conversation as resolved Outdated

Bitte in 2 Funktionen splitten

mapPlayerToAdministratablePlayer <-- Für einen Spieler
Für mehrere Spieler -> mapPlayersToAdministratablePlayers, welche die erste benutzt und den for loop hat

Bitte in 2 Funktionen splitten `mapPlayerToAdministratablePlayer` <-- Für einen Spieler Für mehrere Spieler -> `mapPlayersToAdministratablePlayers`, welche die erste benutzt und den for loop hat
List<AdministratablePlayer> administratablePlayers = new ArrayList<>();
SZUT-Mehdi marked this conversation as resolved Outdated

Variable name ist noch falsch

Variable name ist noch falsch
for (Player player : players) {
AdministratablePlayer apiPlayer = new AdministratablePlayer();
SZUT-Mehdi marked this conversation as resolved Outdated

Variable name ist noch falsch

Variable name ist noch falsch
apiPlayer.setUsername(player.getUsername());
administratablePlayers.add(apiPlayer);
}
return administratablePlayers;
}
public AdministratablePlayer mapPlayerToAdministratablePlayer(Player player) {
AdministratablePlayer administratablePlayer = new AdministratablePlayer();
administratablePlayer.setUsername(player.getUsername());
return administratablePlayer;
}
}

View file

@ -1,7 +1,9 @@
package de.towerdefence.server; package de.towerdefence.server;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import de.towerdefence.server.player.Player;
import de.towerdefence.server.player.PlayerRepository; import de.towerdefence.server.player.PlayerRepository;
import de.towerdefence.server.player.PlayerService;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -10,6 +12,10 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
@SpringBootTest @SpringBootTest
@AutoConfigureMockMvc(addFilters = false) @AutoConfigureMockMvc(addFilters = false)
@ActiveProfiles("test") @ActiveProfiles("test")
@ -23,10 +29,27 @@ public abstract class IntegrationTest {
protected ObjectMapper objectMapper; protected ObjectMapper objectMapper;
@Autowired @Autowired
protected PlayerRepository playerRepository; protected PlayerRepository playerRepository;
@Autowired
protected PlayerService playerService;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
playerRepository.deleteAll(); playerRepository.deleteAll();
SZUT-Mehdi marked this conversation as resolved Outdated

Bitte einen Map und einen Loop nutzen für das Anlegen.

Dies würde den code lesbarer machen

Bitte einen Map und einen Loop nutzen für das Anlegen. Dies würde den code lesbarer machen
Map<String, String> players = new HashMap<>();
players.put("Alex", "1234");
players.put("Zorro", "1234");
players.forEach((username, password) -> {
Player player = new Player();
player.setUsername(username);
try {
playerService.setPassword(player, password);
playerRepository.saveAndFlush(player);
} catch (NoSuchAlgorithmException e) {
System.err.println("Error setting password for player: " + username);
}
});
SZUT-Mehdi marked this conversation as resolved Outdated

Statt dem Save oben, hier direkt nach dem loop einmal ein Save und Flush ausführen:

playerRepository.saveAllAndFlush(players.values());
Statt dem Save oben, hier direkt nach dem loop einmal ein Save und Flush ausführen: ```java playerRepository.saveAllAndFlush(players.values()); ```
} }
SZUT-Mehdi marked this conversation as resolved Outdated

Log entfernen, hat hier nix zu suchen.

Log entfernen, hat hier nix zu suchen.
@AfterEach @AfterEach

View file

@ -0,0 +1,85 @@
package de.towerdefence.server.server;
import de.towerdefence.server.IntegrationTest;
import de.towerdefence.server.oas.models.PlayerFilter;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
class GetAllPlayersTest extends IntegrationTest {
private MockHttpServletRequestBuilder createGetAllPlayersRequest(String requestBody) {
return MockMvcRequestBuilders.get(baseUri + "/admin/players")
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody);
}
@Test
void playersExist() throws Exception {
SZUT-Mehdi marked this conversation as resolved Outdated

Nope, wir schreiben kein Raw JSON.

https://git.euph.dev/SZUT/ProjectManagmentTool/src/branch/trunk/src/test/java/de/hmmh/pmt/project/CreateTest.java#L102

Mithilfe eines Object Mappers kannst du die passenden Objekte direkt in JSON umwandeln, ohne dabei das Problem zu haben, dass du potenziell invalides JSON schreibst.

Außerdem erhöht es die lesbarkeit des Codes

Nope, wir schreiben kein Raw JSON. https://git.euph.dev/SZUT/ProjectManagmentTool/src/branch/trunk/src/test/java/de/hmmh/pmt/project/CreateTest.java#L102 Mithilfe eines Object Mappers kannst du die passenden Objekte direkt in JSON umwandeln, ohne dabei das Problem zu haben, dass du potenziell invalides JSON schreibst. Außerdem erhöht es die lesbarkeit des Codes
PlayerFilter playerFilter = new PlayerFilter();
playerFilter.setPage(0);
playerFilter.setPageSize(10);
playerFilter.setOrder(PlayerFilter.OrderEnum.DESCENDING);
playerFilter.setUsername("");
playerFilter.setSortBy("username");
String requestBody = this.objectMapper.writeValueAsString(playerFilter);
this.mvc.perform(createGetAllPlayersRequest(requestBody))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$[0]").exists());
}
@Test
void playersSortedByAsc() throws Exception {
PlayerFilter playerFilter = new PlayerFilter();
playerFilter.setPage(0);
playerFilter.setPageSize(10);
playerFilter.setOrder(PlayerFilter.OrderEnum.ASCENDING);
playerFilter.setUsername("");
playerFilter.setSortBy("username");
String requestBody = this.objectMapper.writeValueAsString(playerFilter);
this.mvc.perform(createGetAllPlayersRequest(requestBody))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].username").value("Alex"))
.andExpect(jsonPath("$[1].username").value("Zorro"));
}
@Test
void playersSortedByDesc() throws Exception {
PlayerFilter playerFilter = new PlayerFilter();
playerFilter.setPage(0);
playerFilter.setPageSize(10);
playerFilter.setOrder(PlayerFilter.OrderEnum.DESCENDING);
playerFilter.setUsername("");
playerFilter.setSortBy("username");
String requestBody = this.objectMapper.writeValueAsString(playerFilter);
this.mvc.perform(createGetAllPlayersRequest(requestBody))
.andExpect(status().isOk())
.andExpect(jsonPath("$[1].username").value("Alex"))
.andExpect(jsonPath("$[0].username").value("Zorro"));
}
@Test
void playersFiltered() throws Exception {
PlayerFilter playerFilter = new PlayerFilter();
playerFilter.setPage(0);
playerFilter.setPageSize(10);
playerFilter.setOrder(PlayerFilter.OrderEnum.ASCENDING);
playerFilter.setUsername("Alex");
playerFilter.setSortBy("username");
String requestBody = this.objectMapper.writeValueAsString(playerFilter);
this.mvc.perform(createGetAllPlayersRequest(requestBody))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].username").value("Alex"))
.andExpect(jsonPath("$").isNotEmpty());
}
}

View file

@ -30,6 +30,4 @@ public class PlayerRegistrationTest extends IntegrationTest {
.content(this.objectMapper.writeValueAsString(playerRegistrationData)) .content(this.objectMapper.writeValueAsString(playerRegistrationData))
.contentType(MediaType.APPLICATION_JSON); .contentType(MediaType.APPLICATION_JSON);
} }
} }