From 7cb82db311f3aa0b7d902e2b03d504974d327add Mon Sep 17 00:00:00 2001 From: Benoit Lacelle Date: Fri, 13 Sep 2024 23:16:57 +0400 Subject: [PATCH] Tntroduce the Lag realtime game --- .../kumite/game/IGameMetadataConstants.java | 9 ++ .../account/login/FakePlayerTokens.java | 6 +- .../login/KumiteOAuth2UserService.java | 2 +- .../kumite/account/login/KumiteSecurity.java | 11 +- .../account/login/KumiteTokenService.java | 18 +-- .../account/login/KumiteUsersRegistry.java | 18 +-- .../app/controllers/KumiteHandlerHelper.java | 4 + .../controllers/KumiteLoginController.java | 29 +++-- .../webflux/KumiteWebExceptionHandler.java | 62 ++++++++++ .../eu/solven/kumite/board/BoardHandler.java | 2 + .../game/opposition/tictactoe/TicTacToe.java | 1 + .../kumite/game/optimization/ping/Lag.java | 81 +++++++++++++ .../game/optimization/ping/LagBoard.java | 46 ++++++++ .../optimization/ping/LagServerTimestamp.java | 14 +++ .../kumite/leaderboard/PlayerLongScore.java | 15 +++ .../kumite/player/AccountPlayersRegistry.java | 12 +- .../eu/solven/kumite/player/IHasPlayers.java | 12 +- .../src/main/resources/static/ui/js/about.js | 30 ++++- .../static/ui/js/kumite-board-join.js | 6 +- .../static/ui/js/kumite-board-joined.js | 7 +- .../static/ui/js/kumite-board-move-form.js | 26 ++-- .../static/ui/js/kumite-contest-form.js | 20 +++- .../resources/static/ui/js/kumite-contests.js | 4 +- .../ui/js/kumite-stateless-composables.js | 6 +- .../src/main/resources/static/ui/js/store.js | 111 +++++++++--------- .../account/login/TestKumiteTokenService.java | 4 +- .../TestKumiteLoginController.java | 49 ++++++++ .../kumite/app/it/TestKumiteApiRouter.java | 3 +- .../KumiteServerSecurityApplication.java | 4 + .../it/security/TestSecurity_WithJwtUser.java | 3 +- .../security/TestSecurity_WithOAuth2User.java | 10 +- .../scenario/TestAccountPlayersRegistry.java | 4 +- .../scenario/TestBoardLifecycleManager.java | 2 +- 33 files changed, 501 insertions(+), 130 deletions(-) create mode 100644 server/src/main/java/eu/solven/kumite/app/webflux/KumiteWebExceptionHandler.java create mode 100644 server/src/main/java/eu/solven/kumite/game/optimization/ping/Lag.java create mode 100644 server/src/main/java/eu/solven/kumite/game/optimization/ping/LagBoard.java create mode 100644 server/src/main/java/eu/solven/kumite/game/optimization/ping/LagServerTimestamp.java create mode 100644 server/src/main/java/eu/solven/kumite/leaderboard/PlayerLongScore.java create mode 100644 server/src/test/java/eu/solven/kumite/app/controllers/TestKumiteLoginController.java diff --git a/public/src/main/java/eu/solven/kumite/game/IGameMetadataConstants.java b/public/src/main/java/eu/solven/kumite/game/IGameMetadataConstants.java index bcdfb15..37d2422 100644 --- a/public/src/main/java/eu/solven/kumite/game/IGameMetadataConstants.java +++ b/public/src/main/java/eu/solven/kumite/game/IGameMetadataConstants.java @@ -1,11 +1,20 @@ package eu.solven.kumite.game; public interface IGameMetadataConstants { + // An optimization game consists in proposing the best solution to a given problem. They can be played independently + // by any players. String TAG_OPTIMIZATION = "optimization"; + // Many games are `1v1` as the oppose 2 players on a given board. String TAG_1V1 = "1v1"; // https://en.wikipedia.org/wiki/Perfect_information // https://www.reddit.com/r/boardgames/comments/bdi78u/what_are_some_simple_games_with_no_hidden/ String TAG_PERFECT_INFORMATION = "perfect_information"; + + // A turn-based game expects a move from a single player for any state + String TAG_TURNBASED = "turned-based"; + + // A real-time game allows all players to move concurrently. + String TAG_REALTIME = "real-time"; } diff --git a/server/src/main/java/eu/solven/kumite/account/login/FakePlayerTokens.java b/server/src/main/java/eu/solven/kumite/account/login/FakePlayerTokens.java index 1ae0b1d..20698d9 100644 --- a/server/src/main/java/eu/solven/kumite/account/login/FakePlayerTokens.java +++ b/server/src/main/java/eu/solven/kumite/account/login/FakePlayerTokens.java @@ -47,7 +47,7 @@ public static void main(String[] args) { @Bean public Void generateFakePlayerToken(KumiteTokenService tokenService) { - String accessToken = tokenService.generateAccessToken(fakeUser()); + String accessToken = tokenService.generateAccessToken(fakeUser(), KumitePlayer.FAKE_PLAYER_ID); log.info("access_token for fakeUser: {}", accessToken); @@ -69,4 +69,8 @@ public static KumiteUser fakeUser() { .build(); } + public static KumitePlayer fakePlayer() { + return KumitePlayer.builder().playerId(KumitePlayer.FAKE_PLAYER_ID).build(); + } + } diff --git a/server/src/main/java/eu/solven/kumite/account/login/KumiteOAuth2UserService.java b/server/src/main/java/eu/solven/kumite/account/login/KumiteOAuth2UserService.java index 343658b..21bfd82 100644 --- a/server/src/main/java/eu/solven/kumite/account/login/KumiteOAuth2UserService.java +++ b/server/src/main/java/eu/solven/kumite/account/login/KumiteOAuth2UserService.java @@ -34,7 +34,7 @@ public class KumiteOAuth2UserService extends DefaultReactiveOAuth2UserService { // private final AccountsStore accountsStore; - private final KumiteUsersRegistry usersRegistry; + final KumiteUsersRegistry usersRegistry; @Override @SneakyThrows(OAuth2AuthenticationException.class) diff --git a/server/src/main/java/eu/solven/kumite/account/login/KumiteSecurity.java b/server/src/main/java/eu/solven/kumite/account/login/KumiteSecurity.java index 2d2610c..d08d51a 100644 --- a/server/src/main/java/eu/solven/kumite/account/login/KumiteSecurity.java +++ b/server/src/main/java/eu/solven/kumite/account/login/KumiteSecurity.java @@ -3,16 +3,20 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebFilter; import eu.solven.kumite.app.controllers.KumiteLoginController; import eu.solven.kumite.app.controllers.KumitePublicController; import eu.solven.kumite.app.controllers.MetadataController; import eu.solven.kumite.app.webflux.KumiteExceptionRoutingWebFilter; +import eu.solven.kumite.app.webflux.KumiteWebExceptionHandler; // https://docs.spring.io/spring-security/reference/reactive/oauth2/login/advanced.html#webflux-oauth2-login-advanced-userinfo-endpoint @RestController -@Import({ SocialWebFluxSecurity.class, +@Import({ + + SocialWebFluxSecurity.class, KumitePublicController.class, KumiteLoginController.class, @@ -27,4 +31,9 @@ public class KumiteSecurity { WebFilter kumiteExceptionRoutingWebFilter() { return new KumiteExceptionRoutingWebFilter(); } + + @Bean + WebExceptionHandler kumiteWebExceptionHandler() { + return new KumiteWebExceptionHandler(); + } } \ No newline at end of file diff --git a/server/src/main/java/eu/solven/kumite/account/login/KumiteTokenService.java b/server/src/main/java/eu/solven/kumite/account/login/KumiteTokenService.java index 1b40cc5..6680cf3 100644 --- a/server/src/main/java/eu/solven/kumite/account/login/KumiteTokenService.java +++ b/server/src/main/java/eu/solven/kumite/account/login/KumiteTokenService.java @@ -5,6 +5,7 @@ import java.time.Instant; import java.util.Date; import java.util.Map; +import java.util.UUID; import java.util.function.Supplier; import org.springframework.core.env.Environment; @@ -34,12 +35,12 @@ public class KumiteTokenService { public static final String KEY_ACCESSTOKEN_EXP = "kumite.login.oauth2_exp"; final Environment env; - final IUuidGenerator uuidgenerator; + final IUuidGenerator uuidGenerator; final Supplier supplierSymetricKey; public KumiteTokenService(Environment env, IUuidGenerator uuidgenerator) { this.env = env; - this.uuidgenerator = uuidgenerator; + this.uuidGenerator = uuidgenerator; this.supplierSymetricKey = () -> loadSigningJwk(); } @@ -48,9 +49,9 @@ private OctetSequenceKey loadSigningJwk() { return OctetSequenceKey.parse(env.getRequiredProperty(KEY_JWT_SIGNINGKEY)); } - public Map wrapInJwtToken(KumiteUser user) { - String accessToken = generateAccessToken(user); - return Map.of("access_token", accessToken); + public Map wrapInJwtToken(KumiteUser user, UUID playerId) { + String accessToken = generateAccessToken(user, playerId); + return Map.of("access_token", accessToken, "player_id", playerId); } public static void main(String[] args) { @@ -82,13 +83,14 @@ static JWK generateSignatureSecret(IUuidGenerator uuidgenerator) { * * @param user * The user for whom to generate an access token. + * @param playerId * @throws IllegalArgumentException * if provided argument is null. * @return The generated JWT access token. * @throws IllegalStateException */ @SneakyThrows({ JOSEException.class }) - public String generateAccessToken(KumiteUser user) { + public String generateAccessToken(KumiteUser user, UUID playerId) { Duration accessTokenValidity = Duration.parse(env.getProperty(KEY_ACCESSTOKEN_EXP, "PT1H")); if (accessTokenValidity.compareTo(Duration.parse("PT1H")) > 0) { @@ -109,11 +111,11 @@ public String generateAccessToken(KumiteUser user) { JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder().subject(user.getAccountId().toString()) .audience("Kumite-Server") .issuer("https://kumite.com") - .jwtID(uuidgenerator.randomUUID().toString()) + .jwtID(uuidGenerator.randomUUID().toString()) .issueTime(curDate) .notBeforeTime(Date.from(Instant.now())) .expirationTime(Date.from(Instant.now().plusMillis(expirationMs))) - .claim("mainPlayerId", user.getPlayerId().toString()); + .claim("playerId", playerId.toString()); SignedJWT signedJWT = new SignedJWT(headerBuilder.build(), claimsSetBuilder.build()); diff --git a/server/src/main/java/eu/solven/kumite/account/login/KumiteUsersRegistry.java b/server/src/main/java/eu/solven/kumite/account/login/KumiteUsersRegistry.java index 1467fd0..f153939 100644 --- a/server/src/main/java/eu/solven/kumite/account/login/KumiteUsersRegistry.java +++ b/server/src/main/java/eu/solven/kumite/account/login/KumiteUsersRegistry.java @@ -8,21 +8,24 @@ import eu.solven.kumite.account.KumiteUser.KumiteUserBuilder; import eu.solven.kumite.account.KumiteUserRaw; import eu.solven.kumite.account.KumiteUserRawRaw; +import eu.solven.kumite.player.AccountPlayersRegistry; import eu.solven.kumite.player.KumitePlayer; import eu.solven.kumite.tools.IUuidGenerator; -import lombok.Value; +import lombok.RequiredArgsConstructor; -@Value +@RequiredArgsConstructor public class KumiteUsersRegistry { - IUuidGenerator uuidGenerator; + final IUuidGenerator uuidGenerator; + + final AccountPlayersRegistry playersRegistry; // This is a cache of the external information about a user // This is useful to enrich some data about other players (e.g. a Leaderboard) - Map userIdToUser = new ConcurrentHashMap<>(); + final Map userIdToUser = new ConcurrentHashMap<>(); // We may have multiple users for a single account // This maps to the latest/main one - Map accountIdToUser = new ConcurrentHashMap<>(); + final Map accountIdToUser = new ConcurrentHashMap<>(); public KumiteUser getUser(UUID accountId) { KumiteUserRawRaw rawUser = accountIdToUser.get(accountId); @@ -63,14 +66,15 @@ public KumiteUser registerOrUpdate(KumiteUserRaw kumiteUserRaw) { UUID playerId = uuidGenerator.randomUUID(); kumiteUserBuilder.playerId(playerId); + + playersRegistry.registerPlayer(accountId, KumitePlayer.builder().playerId(playerId).build()); + accountIdToUser.putIfAbsent(accountId, rawRaw); } else { kumiteUserBuilder.accountId(alreadyIn.getAccountId()).playerId(alreadyIn.getPlayerId()); } return kumiteUserBuilder.build(); }); - - accountIdToUser.putIfAbsent(kumiteUser.getAccountId(), rawRaw); } return kumiteUser; diff --git a/server/src/main/java/eu/solven/kumite/app/controllers/KumiteHandlerHelper.java b/server/src/main/java/eu/solven/kumite/app/controllers/KumiteHandlerHelper.java index 92cfc67..7f7febe 100644 --- a/server/src/main/java/eu/solven/kumite/app/controllers/KumiteHandlerHelper.java +++ b/server/src/main/java/eu/solven/kumite/app/controllers/KumiteHandlerHelper.java @@ -40,6 +40,10 @@ public static Optional optUuid(ServerRequest request, String idKey) { return optUuid.map(rawUuid -> uuid(rawUuid)); } + public static Optional optUuid(Optional optRaw) { + return optRaw.map(raw -> uuid(raw)); + } + public static Optional optBoolean(ServerRequest request, String idKey) { Optional optBoolean = request.queryParam(idKey); diff --git a/server/src/main/java/eu/solven/kumite/app/controllers/KumiteLoginController.java b/server/src/main/java/eu/solven/kumite/app/controllers/KumiteLoginController.java index 5968639..d3cb9cd 100644 --- a/server/src/main/java/eu/solven/kumite/app/controllers/KumiteLoginController.java +++ b/server/src/main/java/eu/solven/kumite/app/controllers/KumiteLoginController.java @@ -1,7 +1,9 @@ package eu.solven.kumite.app.controllers; import java.util.Map; +import java.util.Optional; import java.util.TreeMap; +import java.util.UUID; import java.util.stream.StreamSupport; import org.springframework.core.env.Environment; @@ -12,6 +14,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import eu.solven.kumite.account.KumiteUser; @@ -21,6 +24,7 @@ import eu.solven.kumite.account.login.KumiteUsersRegistry; import eu.solven.kumite.app.IKumiteSpringProfiles; import eu.solven.kumite.app.webflux.LoginRouteButNotAuthenticatedException; +import eu.solven.kumite.player.AccountPlayersRegistry; import lombok.AllArgsConstructor; import reactor.core.publisher.Mono; @@ -31,16 +35,11 @@ public class KumiteLoginController { final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository; final KumiteUsersRegistry usersRegistry; + final AccountPlayersRegistry playersRegistry; final Environment env; final KumiteTokenService kumiteTokenService; - // Redirect to the UI route showing the User how to login - // @GetMapping - // public ResponseEntity login() { - // return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).header(HttpHeaders.LOCATION, "/login").build(); - // } - @GetMapping("/providers") public Map loginProviders() { Map registrationIdToDetails = new TreeMap<>(); @@ -89,8 +88,22 @@ private String guessProviderId(OAuth2User o) { } @GetMapping("/token") - public Mono> token(@AuthenticationPrincipal Mono oauth2User) { - return user(oauth2User).map(user -> kumiteTokenService.wrapInJwtToken(user)); + public Mono> token(@AuthenticationPrincipal Mono oauth2User, + @RequestParam(name = "player_id", required = false) String rawPlayerId) { + return user(oauth2User).map(user -> { + UUID playerId = KumiteHandlerHelper.optUuid(Optional.ofNullable(rawPlayerId)).orElse(user.getPlayerId()); + + checkValidPlayerId(user, playerId); + + return kumiteTokenService.wrapInJwtToken(user, playerId); + }); + } + + void checkValidPlayerId(KumiteUser user, UUID playerId) { + UUID accountId = user.getAccountId(); + if (!playersRegistry.makeDynamicHasPlayers(accountId).hasPlayerId(playerId)) { + throw new IllegalArgumentException("player_id=" + playerId + " is not managed by accountId=" + accountId); + } } } \ No newline at end of file diff --git a/server/src/main/java/eu/solven/kumite/app/webflux/KumiteWebExceptionHandler.java b/server/src/main/java/eu/solven/kumite/app/webflux/KumiteWebExceptionHandler.java new file mode 100644 index 0000000..b069a19 --- /dev/null +++ b/server/src/main/java/eu/solven/kumite/app/webflux/KumiteWebExceptionHandler.java @@ -0,0 +1,62 @@ +package eu.solven.kumite.app.webflux; + +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.resource.NoResourceFoundException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebExceptionHandler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Component +// '-2' to have higher priority than the default WebExceptionHandler +@Order(-2) +@Slf4j +public class KumiteWebExceptionHandler implements WebExceptionHandler { + + @Override + public Mono handle(ServerWebExchange exchange, Throwable e) { + if (e instanceof NoResourceFoundException) { + // Let the default WebExceptionHandler manage 404 + return Mono.error(e); + } else if (e instanceof IllegalArgumentException) { + exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST); + } else if (e instanceof LoginRouteButNotAuthenticatedException) { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + } else { + exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + } + + Map responseBody = new LinkedHashMap<>(); + + if (e.getMessage() == null) { + responseBody.put("error_message", ""); + } else { + responseBody.put("error_message", e.getMessage()); + } + + String respondyBodyAsString; + try { + respondyBodyAsString = new ObjectMapper().writeValueAsString(responseBody); + } catch (JsonProcessingException ee) { + log.error("Issue producing responseBody given {}", responseBody, ee); + respondyBodyAsString = "{\"error_message\":\"something_went_very_wrong\"}"; + } + + byte[] bytes = respondyBodyAsString.getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); + return exchange.getResponse().writeWith(Flux.just(buffer)); + } + +} \ No newline at end of file diff --git a/server/src/main/java/eu/solven/kumite/board/BoardHandler.java b/server/src/main/java/eu/solven/kumite/board/BoardHandler.java index 28aac78..809b1ee 100644 --- a/server/src/main/java/eu/solven/kumite/board/BoardHandler.java +++ b/server/src/main/java/eu/solven/kumite/board/BoardHandler.java @@ -50,6 +50,8 @@ public Mono getBoard(ServerRequest request) { ContestSearchParametersBuilder parameters = ContestSearchParameters.builder(); List contest = contestsRegistry.searchContests(parameters.contestId(Optional.of(contestId)).build()); if (contest.isEmpty()) { + // https://stackoverflow.com/questions/5604816/whats-the-most-appropriate-http-status-code-for-an-item-not-found-error-page + // We may want a specific exception + httpStatusCode throw new IllegalArgumentException("No contest for contestId=" + contestId); } else if (contest.size() >= 2) { throw new IllegalStateException("Multiple contests for contestId=" + contestId + " contests=" + contest); diff --git a/server/src/main/java/eu/solven/kumite/game/opposition/tictactoe/TicTacToe.java b/server/src/main/java/eu/solven/kumite/game/opposition/tictactoe/TicTacToe.java index fe95d74..386e9a1 100644 --- a/server/src/main/java/eu/solven/kumite/game/opposition/tictactoe/TicTacToe.java +++ b/server/src/main/java/eu/solven/kumite/game/opposition/tictactoe/TicTacToe.java @@ -26,6 +26,7 @@ public class TicTacToe implements IGame { .title("Tic-Tac-Toe") .tag(IGameMetadataConstants.TAG_1V1) .tag(IGameMetadataConstants.TAG_PERFECT_INFORMATION) + .tag(IGameMetadataConstants.TAG_TURNBASED) .minPlayers(2) .maxPlayers(2) .shortDescription( diff --git a/server/src/main/java/eu/solven/kumite/game/optimization/ping/Lag.java b/server/src/main/java/eu/solven/kumite/game/optimization/ping/Lag.java new file mode 100644 index 0000000..2e1d0a3 --- /dev/null +++ b/server/src/main/java/eu/solven/kumite/game/optimization/ping/Lag.java @@ -0,0 +1,81 @@ +package eu.solven.kumite.game.optimization.ping; + +import java.net.URI; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.random.RandomGenerator; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import eu.solven.kumite.board.IHasBoard; +import eu.solven.kumite.board.IKumiteBoard; +import eu.solven.kumite.board.IKumiteBoardView; +import eu.solven.kumite.contest.IHasGameover; +import eu.solven.kumite.game.GameMetadata; +import eu.solven.kumite.game.IGame; +import eu.solven.kumite.game.IGameMetadataConstants; +import eu.solven.kumite.leaderboard.IPlayerScore; +import eu.solven.kumite.leaderboard.LeaderBoard; +import eu.solven.kumite.leaderboard.PlayerLongScore; +import eu.solven.kumite.player.IKumiteMove; +import lombok.Value; + +@Value +public class Lag implements IGame { + GameMetadata gameMetadata = GameMetadata.builder() + .gameId(UUID.fromString("b606f730-cce4-44c7-a41f-44d557bd517f")) + .title("Lag") + .tag(IGameMetadataConstants.TAG_OPTIMIZATION) + .tag(IGameMetadataConstants.TAG_PERFECT_INFORMATION) + .tag(IGameMetadataConstants.TAG_REALTIME) + .maxPlayers(Integer.MAX_VALUE) + .shortDescription("You score your Lag. This is the simplest realtime game") + .reference(URI.create("https://en.wikipedia.org/wiki/Lag_(video_games)")) + .build(); + + @Override + public boolean isValidMove(IKumiteMove move) { + return true; + } + + @Override + public LagBoard generateInitialBoard(RandomGenerator random) { + return LagBoard.builder().build(); + } + + @Override + public LagServerTimestamp parseRawMove(Map rawMove) { + return new ObjectMapper().convertValue(rawMove, LagServerTimestamp.class); + } + + @Override + public IKumiteBoard parseRawBoard(Map rawBoard) { + return new ObjectMapper().convertValue(rawBoard, LagBoard.class); + } + + @Override + public LeaderBoard makeLeaderboard(IKumiteBoard board) { + Map playerToScore = new TreeMap<>(); + + LagBoard tspBoard = (LagBoard) board; + tspBoard.getPlayerToLatestLagMs().forEach((playerId, lag) -> { + long score = lag; + playerToScore.put(playerId, PlayerLongScore.builder().playerId(playerId).score(score).build()); + }); + + return LeaderBoard.builder().playerIdToPlayerScore(playerToScore).build(); + } + + @Override + public Map exampleMoves(IKumiteBoardView boardView, UUID playerId) { + return Map.of("now", + LagServerTimestamp.builder().moveTimestamp(Long.toString(System.currentTimeMillis())).build()); + } + + @Override + public IHasGameover makeDynamicGameover(IHasBoard rawBoard) { + // TODO Implement a timeout logic + return () -> false; + } +} diff --git a/server/src/main/java/eu/solven/kumite/game/optimization/ping/LagBoard.java b/server/src/main/java/eu/solven/kumite/game/optimization/ping/LagBoard.java new file mode 100644 index 0000000..1c09b2d --- /dev/null +++ b/server/src/main/java/eu/solven/kumite/game/optimization/ping/LagBoard.java @@ -0,0 +1,46 @@ +package eu.solven.kumite.game.optimization.ping; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import eu.solven.kumite.board.IKumiteBoard; +import eu.solven.kumite.board.IKumiteBoardView; +import eu.solven.kumite.player.PlayerMoveRaw; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@Value +@Builder +@Jacksonized +public class LagBoard implements IKumiteBoard, IKumiteBoardView { + + Map playerToLatestLagMs = new ConcurrentHashMap<>(); + + @Override + public List isValidMove(PlayerMoveRaw playerMove) { + return Collections.emptyList(); + } + + @Override + public void registerMove(PlayerMoveRaw playerMove) { + LagServerTimestamp tsmSolution = (LagServerTimestamp) playerMove.getMove(); + + long lag = System.currentTimeMillis() - Long.parseLong(tsmSolution.getMoveTimestamp()); + + playerToLatestLagMs.put(playerMove.getPlayerId(), lag); + } + + @Override + public IKumiteBoardView asView(UUID playerId) { + return this; + } + + @Override + public void registerPlayer(UUID playerId) { + // Optimization problems can accept any player + } +} diff --git a/server/src/main/java/eu/solven/kumite/game/optimization/ping/LagServerTimestamp.java b/server/src/main/java/eu/solven/kumite/game/optimization/ping/LagServerTimestamp.java new file mode 100644 index 0000000..5620f39 --- /dev/null +++ b/server/src/main/java/eu/solven/kumite/game/optimization/ping/LagServerTimestamp.java @@ -0,0 +1,14 @@ +package eu.solven.kumite.game.optimization.ping; + +import eu.solven.kumite.player.IKumiteMove; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@Value +@Builder +@Jacksonized +public class LagServerTimestamp implements IKumiteMove { + // Typically generated by the server, we evaluate the lag for this date to go to the player and being sent back + String moveTimestamp; +} diff --git a/server/src/main/java/eu/solven/kumite/leaderboard/PlayerLongScore.java b/server/src/main/java/eu/solven/kumite/leaderboard/PlayerLongScore.java new file mode 100644 index 0000000..f0f3ed9 --- /dev/null +++ b/server/src/main/java/eu/solven/kumite/leaderboard/PlayerLongScore.java @@ -0,0 +1,15 @@ +package eu.solven.kumite.leaderboard; + +import java.util.UUID; + +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@Builder +@Value +@Jacksonized +public class PlayerLongScore implements IPlayerScore { + UUID playerId; + long score; +} diff --git a/server/src/main/java/eu/solven/kumite/player/AccountPlayersRegistry.java b/server/src/main/java/eu/solven/kumite/player/AccountPlayersRegistry.java index 68b0efb..84e4fc6 100644 --- a/server/src/main/java/eu/solven/kumite/player/AccountPlayersRegistry.java +++ b/server/src/main/java/eu/solven/kumite/player/AccountPlayersRegistry.java @@ -8,16 +8,16 @@ import java.util.stream.Collectors; import eu.solven.kumite.account.KumiteUser; -import eu.solven.kumite.game.GamesRegistry; -import lombok.AllArgsConstructor; - -@AllArgsConstructor -public class AccountPlayersRegistry { - final GamesRegistry gamesRegistry; +import eu.solven.kumite.account.login.FakePlayerTokens; +public final class AccountPlayersRegistry { final Map> accountToPlayers = new ConcurrentHashMap<>(); final Map playerIdToAccountId = new ConcurrentHashMap<>(); + public AccountPlayersRegistry() { + registerPlayer(KumiteUser.FAKE_ACCOUNT_ID, FakePlayerTokens.fakePlayer()); + } + public void registerPlayer(UUID accountId, KumitePlayer player) { // Synchronized to make atomic changes to both `accountToPlayers` and `playerIdToAccountId` synchronized (this) { diff --git a/server/src/main/java/eu/solven/kumite/player/IHasPlayers.java b/server/src/main/java/eu/solven/kumite/player/IHasPlayers.java index a4d3506..0ac9df9 100644 --- a/server/src/main/java/eu/solven/kumite/player/IHasPlayers.java +++ b/server/src/main/java/eu/solven/kumite/player/IHasPlayers.java @@ -1,13 +1,23 @@ package eu.solven.kumite.player; import java.util.List; +import java.util.UUID; /** - * Generally provided by {@link ContestPlayersRegistry} + * Generally provided by {@link ContestPlayersRegistry} (or {@link AccountPlayersRegistry}). * * @author Benoit Lacelle * */ public interface IHasPlayers { List getPlayers(); + + /** + * + * @param playerId + * @return true if the playerId is amongst the players. + */ + default boolean hasPlayerId(UUID playerId) { + return getPlayers().stream().anyMatch(p -> p.getPlayerId().equals(playerId)); + } } diff --git a/server/src/main/resources/static/ui/js/about.js b/server/src/main/resources/static/ui/js/about.js index 39981ca..55461f1 100644 --- a/server/src/main/resources/static/ui/js/about.js +++ b/server/src/main/resources/static/ui/js/about.js @@ -11,14 +11,40 @@ export default { template: `

Kumite

This is a plateform for bots/algorithms contests. + + Links + + Lexicon + +
  • - OpenAPI + Game: a set of rules defining winning, losing or scoring conditions. A Game is not played by itself, but on a per-contest basis.
  • +
  • + User: a human User, connected for instance through its Github account. +
  • +
  • + Player: an robot identifier, attached to a single User, and able to join contests. +
  • +
  • + Contest: an occurence of a `Game`, enabling `players` to join. A player may also join as a `viewer`: then, he can not make any move, but it can see the whole `board`. +
  • +
  • + Board: each contest has a `board`, which holds all the information about the state of the contest from the `game` perspective. +
  • +
  • + Move: an action which can be done by a `player` on a given `board`. +
`, }; diff --git a/server/src/main/resources/static/ui/js/kumite-board-join.js b/server/src/main/resources/static/ui/js/kumite-board-join.js index b49553f..aaca42c 100644 --- a/server/src/main/resources/static/ui/js/kumite-board-join.js +++ b/server/src/main/resources/static/ui/js/kumite-board-join.js @@ -34,13 +34,13 @@ export default { ]), ...mapState(useKumiteStore, { game(store) { - return store.games[this.gameId]; + return store.games[this.gameId] || { status: "not_loaded" }; }, contest(store) { - return store.contests[this.contestId]; + return store.contests[this.contestId] || { status: "not_loaded" }; }, board(store) { - return store.boards[this.contestId]; + return store.contests[this.contestId].board || { status: "not_loaded" }; }, }), curlGetBoard() { diff --git a/server/src/main/resources/static/ui/js/kumite-board-joined.js b/server/src/main/resources/static/ui/js/kumite-board-joined.js index 604dfa7..f22a620 100644 --- a/server/src/main/resources/static/ui/js/kumite-board-joined.js +++ b/server/src/main/resources/static/ui/js/kumite-board-joined.js @@ -1,4 +1,4 @@ -import { ref, onMounted, onUnmounted } from "vue"; +import { ref, computed, onMounted, onUnmounted } from "vue"; import { mapState } from "pinia"; import { useKumiteStore } from "./store.js"; @@ -50,8 +50,9 @@ export default { const game = ref(store.games[props.gameId]); const contest = ref(store.contests[props.contestId]); - const requiringPlayers = ref( - contest.value.dynamicMetadata.requiringPlayers, + // https://github.com/vuejs/core/issues/5818 + const requiringPlayers = computed( + () => contest.value.dynamicMetadata.requiringPlayers, ); const shortPollBoardInterval = ref(null); diff --git a/server/src/main/resources/static/ui/js/kumite-board-move-form.js b/server/src/main/resources/static/ui/js/kumite-board-move-form.js index f6a33be..23a48e4 100644 --- a/server/src/main/resources/static/ui/js/kumite-board-move-form.js +++ b/server/src/main/resources/static/ui/js/kumite-board-move-form.js @@ -54,7 +54,7 @@ export default { return store.contests[this.contestId] || { error: "not_loaded" }; }, board(store) { - return store.boards[this.contestId]?.board || { error: "not_loaded" }; + return store.contests[this.contestId]?.board || { error: "not_loaded" }; }, }), curlPostBoard() { @@ -68,7 +68,6 @@ export default { ); }, }, - emits: ["move-sent"], setup(props, context) { const store = useKumiteStore(); @@ -163,7 +162,7 @@ export default { state.leaderboards[contestId] = {}; } state.leaderboards[contestId].stale = true; - state.boards[contestId].stale = true; + state.contests[contestId].stale = true; }); sendMoveError.value = ""; } catch (e) { @@ -189,11 +188,7 @@ export default { // We need to suggest a move is defined through JSON format const rawMove = ref("{}"); - const showBoardWithMoveAsSvg = ref(false); - store.loadBoard(props.gameId, props.contestId).then((board) => { - // If this board enables SVG, activates it by default - showBoardWithMoveAsSvg.value = store.boards[props.contestId].svg; - }); + store.loadBoard(props.gameId, props.contestId); return { sendMoveError, @@ -204,17 +199,22 @@ export default { rawMove, fillMove, sendMove, - showBoardWithMoveAsSvg, }; }, template: ` -
+
+
Loading contestId={{contestId}}
-
-
- {{game.error || contest.error || board.error}} +
+ +
+ {{game.error || contest.error || board.error}} +
+
+ ??? +
diff --git a/server/src/main/resources/static/ui/js/kumite-contest-form.js b/server/src/main/resources/static/ui/js/kumite-contest-form.js index 385e319..0520e5b 100644 --- a/server/src/main/resources/static/ui/js/kumite-contest-form.js +++ b/server/src/main/resources/static/ui/js/kumite-contest-form.js @@ -35,9 +35,12 @@ export default { const store = useKumiteStore(); const contestName = ref("A nice name for a contest"); + const createdContest = ref({}); const submitContestForm = function () { - const payload = { constant_metadata: { name: this.contestName } }; + const constantMetadata = { + constant_metadata: { name: contestName.value }, + }; console.log("Submitting contestCreation", constantMetadata); @@ -46,7 +49,7 @@ export default { const fetchOptions = { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), + body: JSON.stringify(constantMetadata), }; const response = await store.authenticatedFetch(url, fetchOptions); if (!response.ok) { @@ -58,6 +61,7 @@ export default { } const contest = await response.json(); + console.log("Created contest", contest); { console.log("Registering contestId", contest.contestId); @@ -65,16 +69,18 @@ export default { contests: { ...store.contests, [contest.contestId]: contest }, }); } + + createdContest.value = contest; } catch (e) { console.error("Issue on Network:", e); } } const playerId = store.playingPlayerId; - return postFromUrl(`/api/contests?game_id=${gameId}`); + return postFromUrl(`/api/contests?game_id=${props.gameId}`); }; - return { contestName, submitContestForm }; + return { contestName, submitContestForm, createdContest }; }, template: `
@@ -96,8 +102,12 @@ export default {
Pick a name so your friends can find your contest.
- +
+ +
+ {{createdContest.constantMetadata.name}} +
`, }; diff --git a/server/src/main/resources/static/ui/js/kumite-contests.js b/server/src/main/resources/static/ui/js/kumite-contests.js index 759f0c0..392bc40 100644 --- a/server/src/main/resources/static/ui/js/kumite-contests.js +++ b/server/src/main/resources/static/ui/js/kumite-contests.js @@ -26,12 +26,12 @@ export default { contests(store) { const allContests = Object.values(store.contests); - console.log("allContests", allContests); + console.debug("allContests", allContests); if (this.gameId) { // https://stackoverflow.com/questions/69091869/how-to-filter-an-array-in-array-of-objects-in-javascript return allContests.filter( - (contest) => contest.constantMetadata.gameId == this.gameId, + (contest) => contest.constantMetadata.gameId === this.gameId, ); } else { return allContests; diff --git a/server/src/main/resources/static/ui/js/kumite-stateless-composables.js b/server/src/main/resources/static/ui/js/kumite-stateless-composables.js index 71f5811..b60307d 100644 --- a/server/src/main/resources/static/ui/js/kumite-stateless-composables.js +++ b/server/src/main/resources/static/ui/js/kumite-stateless-composables.js @@ -35,13 +35,13 @@ export default { ]), ...mapState(useKumiteStore, { game(store) { - return store.games[this.gameId]; + return store.games[this.gameId] || { error: "not_loaded" }; }, contest(store) { - return store.contests[this.contestId]; + return store.contests[this.contestId] || { error: "not_loaded" }; }, board(store) { - return store.boards[this.contestId]; + return store.contests[this.contestId]?.board || { error: "not_loaded" }; }, }), curlGetBoard() { diff --git a/server/src/main/resources/static/ui/js/store.js b/server/src/main/resources/static/ui/js/store.js index d1fcc40..41bf569 100644 --- a/server/src/main/resources/static/ui/js/store.js +++ b/server/src/main/resources/static/ui/js/store.js @@ -93,6 +93,7 @@ export const useKumiteStore = defineStore("kumite", { async function fetchFromUrl(url) { store.nbAccountLoading++; try { + // Rely on session for authentication const response = await fetch(url); if (response.ok) { @@ -157,6 +158,7 @@ export const useKumiteStore = defineStore("kumite", { async function fetchFromUrl(url) { store.nbAccountLoading++; try { + // Rely on session for authentication const response = await fetch(url); if (!response.ok) { throw new NetworkError( @@ -191,6 +193,7 @@ export const useKumiteStore = defineStore("kumite", { return tokens; } catch (e) { console.error("Issue on Network: ", e); + return { error: e }; } finally { store.nbAccountLoading--; } @@ -198,7 +201,9 @@ export const useKumiteStore = defineStore("kumite", { return this.ensureUser().then(() => { console.log("We do have a User. Let's fetch tokens"); - return fetchFromUrl("/api/login/v1/token"); + return fetchFromUrl( + `/api/login/v1/token?player_id=${this.playingPlayerId}`, + ); }); }, @@ -273,9 +278,9 @@ export const useKumiteStore = defineStore("kumite", { const user = responseJson; responseJson.forEach((player) => { - console.log("Registering playerId", item.playerId); + console.log("Registering playerId", player.playerId); store.$patch({ - players: { ...store.players, [item.playerId]: item }, + players: { ...store.players, [player.playerId]: player }, }); }); } catch (e) { @@ -285,7 +290,7 @@ export const useKumiteStore = defineStore("kumite", { } } - return fetchFromUrl("/api/players?account_id=" + store.account.accountId); + return fetchFromUrl(`/api/players?account_id=${store.account.accountId}`); }, async loadContestPlayers(contestId) { const store = this; @@ -325,7 +330,7 @@ export const useKumiteStore = defineStore("kumite", { } } - return fetchFromUrl("/api/players?contest_id=" + contestId); + return fetchFromUrl(`/api/players?contest_id=${contestId}`); }, async loadGames(gameId) { @@ -397,7 +402,7 @@ export const useKumiteStore = defineStore("kumite", { store.nbGameFetching--; } } - fetchFromUrl("/api/games?game_id=" + gameId); + return fetchFromUrl("/api/games?game_id=" + gameId); } }, @@ -409,7 +414,7 @@ export const useKumiteStore = defineStore("kumite", { const response = await store.authenticatedFetch(url); const responseJson = await response.json(); - console.log("responseJson", responseJson); + console.debug("responseJson", responseJson); responseJson.forEach((contest) => { console.log("Registering contestId", contest.contestId); @@ -469,8 +474,9 @@ export const useKumiteStore = defineStore("kumite", { const responseJson = await response.json(); if (responseJson.length === 0) { - throw new Error("Unknown contestId: " + contestId); + return { contestId: contestId, status: "unknown" }; } else if (responseJson.length !== 1) { + // This should not happen as we provided an input contestId console.error("We expected a single contest", responseJson); return { contestId: contestId, status: "unknown" }; } @@ -501,14 +507,14 @@ export const useKumiteStore = defineStore("kumite", { }, async loadContestIfMissing(gameId, contestId) { - this.loadGameIfMissing(gameId); - - if (this.contests[contestId]) { - console.debug("Skip loading contestId=", contestId); - return Promise.resolve(this.contests[contestId]); - } else { - return this.loadContest(gameId, contestId); - } + return this.loadGameIfMissing(gameId).then((game) => { + if (this.contests[contestId]) { + console.debug("Skip loading contestId=", contestId); + return Promise.resolve(this.contests[contestId]); + } else { + return this.loadContest(gameId, contestId); + } + }); }, async loadBoard(gameId, contestId, playerId) { @@ -519,53 +525,43 @@ export const useKumiteStore = defineStore("kumite", { const store = this; - return this.loadGameIfMissing(gameId) - .then((game) => { - return this.loadContestIfMissing(gameId, contestId); - }) - .then((contest) => { - async function fetchFromUrl(url) { - store.nbBoardFetching++; - try { - const response = await store.authenticatedFetch(url); - if (!response.ok) { - throw new NetworkError( - "Rejected request for contest: " + contestId, - url, - response, - ); - } + return this.loadContestIfMissing(gameId, contestId).then((contest) => { + if (contest.status === "unknown") { + return contest; + } + + async function fetchFromUrl(url) { + store.nbBoardFetching++; + try { + const response = await store.authenticatedFetch(url); + if (!response.ok) { + throw new NetworkError( + "Rejected request for board: " + contestId, + url, + response, + ); + } - const responseJson = await response.json(); - const contestWithBoard = responseJson; + const responseJson = await response.json(); + const contestWithBoard = responseJson; - return contestWithBoard; - } catch (e) { - console.error("Issue on Fetch: ", e.response.status, e); + return contestWithBoard; + } catch (e) { + console.error("Issue on Fetch: ", e.response.status, e); - return { contestId: contestId, status: "error", error: e }; - } finally { - store.nbBoardFetching--; - } + return { contestId: contestId, status: "error", error: e }; + } finally { + store.nbBoardFetching--; } + } - return fetchFromUrl( - "/api/board?game_id=" + - gameId + - "&contest_id=" + - contestId + - "&player_id=" + - playerId, - ).then((contestWithBoard) => { - return this.mergeContest(contestWithBoard); - }); - }); + return fetchFromUrl( + `/api/board?game_id=${gameId}&contest_id=${contestId}&player_id=${playerId}`, + ).then((contestWithBoard) => this.mergeContest(contestWithBoard)); + }); }, async loadLeaderboard(gameId, contestId) { - this.loadGameIfMissing(gameId); - this.loadContestIfMissing(gameId, contestId); - const store = this; async function fetchFromUrl(url) { @@ -612,7 +608,10 @@ export const useKumiteStore = defineStore("kumite", { store.nbLeaderboardFetching--; } } - return fetchFromUrl("/api/leaderboards?contest_id=" + contestId); + + return this.loadContestIfMissing(gameId, contestId).then((contest) => + fetchFromUrl("/api/leaderboards?contest_id=" + contestId), + ); }, }, }); diff --git a/server/src/test/java/eu/solven/kumite/account/login/TestKumiteTokenService.java b/server/src/test/java/eu/solven/kumite/account/login/TestKumiteTokenService.java index 43658d7..c8d3bcc 100644 --- a/server/src/test/java/eu/solven/kumite/account/login/TestKumiteTokenService.java +++ b/server/src/test/java/eu/solven/kumite/account/login/TestKumiteTokenService.java @@ -35,7 +35,7 @@ public void testJwt_randomSecret() throws JOSEException, ParseException { UUID playerId = UUID.randomUUID(); KumiteUser user = KumiteUser.builder().accountId(accountId).playerId(playerId).raw(TestTSPLifecycle.userRaw()).build(); - String accessToken = tokenService.generateAccessToken(user); + String accessToken = tokenService.generateAccessToken(user, playerId); { JWSVerifier verifier = new MACVerifier((OctetSequenceKey) signatureSecret); @@ -53,6 +53,6 @@ public void testJwt_randomSecret() throws JOSEException, ParseException { Jwt jwt = (Jwt) auth.getPrincipal(); Assertions.assertThat(jwt.getSubject()).isEqualTo(accountId.toString()); Assertions.assertThat(jwt.getAudience()).containsExactly("Kumite-Server"); - Assertions.assertThat(jwt.getClaimAsString("mainPlayerId")).isEqualTo(playerId.toString()); + Assertions.assertThat(jwt.getClaimAsString("playerId")).isEqualTo(playerId.toString()); } } diff --git a/server/src/test/java/eu/solven/kumite/app/controllers/TestKumiteLoginController.java b/server/src/test/java/eu/solven/kumite/app/controllers/TestKumiteLoginController.java new file mode 100644 index 0000000..7b9e72f --- /dev/null +++ b/server/src/test/java/eu/solven/kumite/app/controllers/TestKumiteLoginController.java @@ -0,0 +1,49 @@ +package eu.solven.kumite.app.controllers; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.core.env.Environment; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; + +import eu.solven.kumite.account.login.FakePlayerTokens; +import eu.solven.kumite.account.login.KumiteTokenService; +import eu.solven.kumite.account.login.KumiteUsersRegistry; +import eu.solven.kumite.player.AccountPlayersRegistry; +import eu.solven.kumite.tools.IUuidGenerator; +import eu.solven.kumite.tools.JdkUuidGenerator; + +public class TestKumiteLoginController { + final ClientRegistration someClientRegistration = ClientRegistration.withRegistrationId("someRegistrationId") + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .clientId("someClientId") + .tokenUri("someTokenUri") + .build(); + + final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository = + new InMemoryReactiveClientRegistrationRepository(someClientRegistration); + + final IUuidGenerator uuidGenerator = new JdkUuidGenerator(); + + final AccountPlayersRegistry playersRegistry = new AccountPlayersRegistry(); + final KumiteUsersRegistry usersRegistry = new KumiteUsersRegistry(uuidGenerator, playersRegistry); + final Environment env = new MockEnvironment(); + + final KumiteTokenService kumiteTokenService = new KumiteTokenService(env, uuidGenerator); + + final KumiteLoginController controller = new KumiteLoginController(clientRegistrationRepository, + usersRegistry, + playersRegistry, + env, + kumiteTokenService); + + @Test + public void testPlayer_invalid() { + Assertions + .assertThatThrownBy( + () -> controller.checkValidPlayerId(FakePlayerTokens.fakeUser(), uuidGenerator.randomUUID())) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/server/src/test/java/eu/solven/kumite/app/it/TestKumiteApiRouter.java b/server/src/test/java/eu/solven/kumite/app/it/TestKumiteApiRouter.java index 691c43a..5f33d3a 100644 --- a/server/src/test/java/eu/solven/kumite/app/it/TestKumiteApiRouter.java +++ b/server/src/test/java/eu/solven/kumite/app/it/TestKumiteApiRouter.java @@ -22,6 +22,7 @@ import eu.solven.kumite.game.GameMetadata; import eu.solven.kumite.game.GameSearchHandler; import eu.solven.kumite.game.optimization.tsp.TravellingSalesmanProblem; +import eu.solven.kumite.player.KumitePlayer; import lombok.extern.slf4j.Slf4j; @ExtendWith(SpringExtension.class) @@ -37,7 +38,7 @@ public class TestKumiteApiRouter { KumiteTokenService tokenService; protected String generateAccessToken() { - return tokenService.generateAccessToken(FakePlayerTokens.fakeUser()); + return tokenService.generateAccessToken(FakePlayerTokens.fakeUser(), KumitePlayer.FAKE_PLAYER_ID); } @Test diff --git a/server/src/test/java/eu/solven/kumite/app/it/security/KumiteServerSecurityApplication.java b/server/src/test/java/eu/solven/kumite/app/it/security/KumiteServerSecurityApplication.java index e57465e..fcf4a2a 100644 --- a/server/src/test/java/eu/solven/kumite/app/it/security/KumiteServerSecurityApplication.java +++ b/server/src/test/java/eu/solven/kumite/app/it/security/KumiteServerSecurityApplication.java @@ -9,6 +9,7 @@ import eu.solven.kumite.app.KumiteRandomConfiguration; import eu.solven.kumite.app.greeting.GreetingHandler; import eu.solven.kumite.app.webflux.KumiteSpaRouter; +import eu.solven.kumite.player.AccountPlayersRegistry; @SpringBootApplication @Import({ KumiteRandomConfiguration.class, @@ -20,6 +21,9 @@ KumiteSpaRouter.class, GreetingHandler.class, + // This is needed as security often checks the players of an account + AccountPlayersRegistry.class, + }) public class KumiteServerSecurityApplication { diff --git a/server/src/test/java/eu/solven/kumite/app/it/security/TestSecurity_WithJwtUser.java b/server/src/test/java/eu/solven/kumite/app/it/security/TestSecurity_WithJwtUser.java index 94746b1..34d49d0 100644 --- a/server/src/test/java/eu/solven/kumite/app/it/security/TestSecurity_WithJwtUser.java +++ b/server/src/test/java/eu/solven/kumite/app/it/security/TestSecurity_WithJwtUser.java @@ -25,6 +25,7 @@ import eu.solven.kumite.app.IKumiteSpringProfiles; import eu.solven.kumite.app.controllers.KumiteLoginController; import eu.solven.kumite.app.greeting.GreetingHandler; +import eu.solven.kumite.player.KumitePlayer; import lombok.extern.slf4j.Slf4j; /** @@ -49,7 +50,7 @@ public class TestSecurity_WithJwtUser { KumiteTokenService tokenService; protected String generateAccessToken() { - return tokenService.generateAccessToken(FakePlayerTokens.fakeUser()); + return tokenService.generateAccessToken(FakePlayerTokens.fakeUser(), KumitePlayer.FAKE_PLAYER_ID); } @Test diff --git a/server/src/test/java/eu/solven/kumite/app/it/security/TestSecurity_WithOAuth2User.java b/server/src/test/java/eu/solven/kumite/app/it/security/TestSecurity_WithOAuth2User.java index 9948dd3..4891ebb 100644 --- a/server/src/test/java/eu/solven/kumite/app/it/security/TestSecurity_WithOAuth2User.java +++ b/server/src/test/java/eu/solven/kumite/app/it/security/TestSecurity_WithOAuth2User.java @@ -20,6 +20,7 @@ import org.springframework.test.web.reactive.server.StatusAssertions; import org.springframework.test.web.reactive.server.WebTestClient; +import eu.solven.kumite.account.KumiteUser; import eu.solven.kumite.account.KumiteUserRaw; import eu.solven.kumite.account.login.KumiteOAuth2UserService; import eu.solven.kumite.account.login.SocialWebFluxSecurity; @@ -157,6 +158,8 @@ public void testLoginUser() { public void testLoginToken() { log.debug("About {}", KumiteLoginController.class); + KumiteUser kumiteUser; + // Beware `.mutateWith(oauth2Login)` skips KumiteOAuth2UserService, hence automated registration on first OAuth2 // login OAuth2LoginMutator oauth2Login; @@ -166,7 +169,7 @@ public void testLoginToken() { attributes.put("id", userRaw.getRawRaw().getSub()); attributes.put("providerId", userRaw.getRawRaw().getProviderId()); }); - oauth2UserService.onKumiteUserRaw(userRaw); + kumiteUser = oauth2UserService.onKumiteUserRaw(userRaw); } webTestClient @@ -182,7 +185,10 @@ public void testLoginToken() { .isOk() .expectBody(Map.class) .value(tokens -> { - Assertions.assertThat(tokens).containsKey("access_token").hasSize(1); + Assertions.assertThat(tokens) + .containsKey("access_token") + .containsEntry("player_id", kumiteUser.getPlayerId().toString()) + .hasSize(2); }); } diff --git a/server/src/test/java/eu/solven/kumite/scenario/TestAccountPlayersRegistry.java b/server/src/test/java/eu/solven/kumite/scenario/TestAccountPlayersRegistry.java index 05861d9..e975cce 100644 --- a/server/src/test/java/eu/solven/kumite/scenario/TestAccountPlayersRegistry.java +++ b/server/src/test/java/eu/solven/kumite/scenario/TestAccountPlayersRegistry.java @@ -4,13 +4,11 @@ import org.junit.jupiter.api.Test; import eu.solven.kumite.account.KumiteUser; -import eu.solven.kumite.game.GamesRegistry; import eu.solven.kumite.player.AccountPlayersRegistry; import eu.solven.kumite.player.KumitePlayer; public class TestAccountPlayersRegistry { - GamesRegistry gamesRegistry = new GamesRegistry(); - AccountPlayersRegistry playersRegistry = new AccountPlayersRegistry(gamesRegistry); + AccountPlayersRegistry playersRegistry = new AccountPlayersRegistry(); @Test public void testFakePlayer() { diff --git a/server/src/test/java/eu/solven/kumite/scenario/TestBoardLifecycleManager.java b/server/src/test/java/eu/solven/kumite/scenario/TestBoardLifecycleManager.java index 69516cc..93b75b6 100644 --- a/server/src/test/java/eu/solven/kumite/scenario/TestBoardLifecycleManager.java +++ b/server/src/test/java/eu/solven/kumite/scenario/TestBoardLifecycleManager.java @@ -32,7 +32,7 @@ public class TestBoardLifecycleManager { BoardsRegistry boardRegistry = new BoardsRegistry(); GamesRegistry gamesRegistry = new GamesRegistry(); - AccountPlayersRegistry playersRegistry = new AccountPlayersRegistry(gamesRegistry); + AccountPlayersRegistry playersRegistry = new AccountPlayersRegistry(); ContestPlayersRegistry contestPlayersRegistry = new ContestPlayersRegistry(gamesRegistry, playersRegistry);