Skip to content

Commit

Permalink
Add an integration test going through the API
Browse files Browse the repository at this point in the history
  • Loading branch information
blacelle committed Sep 11, 2024
1 parent 7d67742 commit 01dc300
Show file tree
Hide file tree
Showing 31 changed files with 850 additions and 185 deletions.
5 changes: 5 additions & 0 deletions monolith/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
3 changes: 2 additions & 1 deletion monolith/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ spring:
- "default_player"
- "default_server"
- "server"
- "fake_player"
- "fake_player"
- inject_default_games
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package eu.solven.kumite.app.it;

import java.util.Map;
import java.util.Optional;
import java.util.UUID;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.core.env.Environment;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import eu.solven.kumite.app.IKumiteServer;
import eu.solven.kumite.app.IKumiteSpringProfiles;
import eu.solven.kumite.app.KumiteServerApplication;
import eu.solven.kumite.app.KumiteWebclientServer;
import eu.solven.kumite.contest.ContestSearchParameters;
import eu.solven.kumite.game.GameSearchParameters;
import eu.solven.kumite.player.PlayerRawMovesHolder;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;

/**
* This integration-test serves 2 purposes: first it shows how one can chain call to play a game: it can help ensure the
* API is stable and simple; second, it ensures the API is actually functional (e.g. up to serializibility of involved
* classes).
*
* @author Benoit Lacelle
* @see 'TestTSPLifecycle'
*/
// Should this move to `monolith` module?
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = KumiteServerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles({ IKumiteSpringProfiles.P_DEFAULT, IKumiteSpringProfiles.P_DEFAULT_FAKE_PLAYER, })
@TestPropertySource(properties = { "kumite.random.seed=123",
"kumite.server.base-url=http://localhost:LocalServerPort",
"kumite.random.seed=123" })
@Slf4j
public class TestTSPLifecycleThroughRouter {

// https://stackoverflow.com/questions/30312058/spring-boot-how-to-get-the-running-port
@LocalServerPort
int randomServerPort;

@Autowired
Environment env;

@Test
public void testSinglePlayer() {
IKumiteServer kumiteServer = new KumiteWebclientServer(env, randomServerPort);

UUID playerId = env.getRequiredProperty("kumite.playerId", UUID.class);

kumiteServer
// Search for games given a human-friendly pattern
.searchGames(GameSearchParameters.builder().titleRegex(Optional.of(".*Salesman.*")).build())
// Search for contest
.flatMap(game -> {
log.info("Processing game={}", game);
return kumiteServer.searchContests(
ContestSearchParameters.builder().gameId(Optional.of(game.getGameId())).build());
})
// Filter relevant contests
.filter(c -> {
// log.info("c={}", c);
return true;
})
.filter(c -> c.getDynamicMetadata().isAcceptingPlayers())
.filter(c -> !c.getDynamicMetadata().isGameOver())
// Join each relevant contest
.flatMap(contest -> {
log.info("Joining contest={}", contest);
return kumiteServer.joinContest(playerId, contest.getContestId()).flatMap(playerPlayer -> {
log.info("playerPlayer={}", playerPlayer);

return kumiteServer.loadBoard(playerId, contest.getContestId()).flatMap(joinedContest -> {
log.info("Received board for contest={}", joinedContest.getContestId());

Mono<PlayerRawMovesHolder> exampleMoves =
kumiteServer.getExampleMoves(playerId, joinedContest.getContestId());

return exampleMoves.flatMap(moves -> {
Optional<Map<String, ?>> someMove = moves.getMoves().values().stream().findAny();
return Mono.justOrEmpty(someMove);
}).flatMap(move -> {
return kumiteServer.playMove(playerId, joinedContest.getContestId(), move);
});
});
});
})

.doOnError(t -> {
log.error("Something went wrong", t);
})
.then()
.block();
}
}
9 changes: 8 additions & 1 deletion player/src/main/java/eu/solven/kumite/app/IKumiteServer.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package eu.solven.kumite.app;

import java.util.Map;
import java.util.UUID;

import eu.solven.kumite.contest.ContestMetadataRaw;
import eu.solven.kumite.contest.ContestSearchParameters;
import eu.solven.kumite.contest.ContestView;
import eu.solven.kumite.game.GameMetadata;
import eu.solven.kumite.game.GameSearchParameters;
import eu.solven.kumite.player.PlayerRawMovesHolder;
import eu.solven.kumite.player.PlayingPlayer;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

Expand All @@ -17,6 +20,10 @@ public interface IKumiteServer {

Mono<ContestView> loadBoard(UUID contestId, UUID playerId);

Mono<ContestView> joinContest(UUID playerId, UUID contestId);
Mono<PlayingPlayer> joinContest(UUID playerId, UUID contestId);

Mono<PlayerRawMovesHolder> getExampleMoves(UUID playerId, UUID contestId);

Mono<ContestView> playMove(UUID playerId, UUID contestId, Map<String, ?> move);

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ public IKumiteServer kumiteServer(Environment env) {

@Bean
public Void playTicTacToe(IKumiteServer kumiteServer, Environment env) {
// UUID playerId = UUID.fromString(env.getRequiredProperty(null, env.cl))
UUID playerId = env.getRequiredProperty("kumite.playerId", UUID.class);

ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
Expand All @@ -57,20 +56,24 @@ public Void playTicTacToe(IKumiteServer kumiteServer, Environment env) {
})
.flatMap(game -> kumiteServer.searchContests(
ContestSearchParameters.builder().gameId(Optional.of(game.getGameId())).build()))
.flatMap(contest -> kumiteServer.loadBoard(contest.getContestId(), null))
.flatMap(contest -> kumiteServer.loadBoard(playerId, contest.getContestId()))
.filter(c -> !c.getDynamicMetadata().isGameOver())
.filter(c -> c.getDynamicMetadata().isAcceptingPlayers())
.doOnNext(contestView -> {
UUID contestId = contestView.getContestId();
log.info("Received board for contestId={}", contestId);

if (contestView.getPlayerHasJoined()) {
if (contestView.getPlayingPlayer().isPlayerHasJoined()) {
log.info("Received board for already joined contestId={}", contestId);

playingContests.add(contestId);
contestToDetails.put(contestId, contestView);
} else if (contestView.getPlayerCanJoin()) {
kumiteServer.joinContest(playerId, contestId);
} else if (contestView.getPlayingPlayer().isPlayerCanJoin()) {
log.info("Received board for joinable contestId={}", contestId);
kumiteServer.joinContest(playerId, contestId).subscribe(view -> {
log.info("Received board for joined contestId={}", contestId);
contestToDetails.put(contestId, contestView);
});
}

})
.subscribe(view -> {
log.info("View: {}", view);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package eu.solven.kumite.app;

import java.util.Map;
import java.util.UUID;

import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec;
import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec;

import eu.solven.kumite.contest.ContestMetadataRaw;
import eu.solven.kumite.contest.ContestSearchParameters;
import eu.solven.kumite.contest.ContestView;
import eu.solven.kumite.game.GameMetadata;
import eu.solven.kumite.game.GameSearchParameters;
import eu.solven.kumite.player.PlayerRawMovesHolder;
import eu.solven.kumite.player.PlayingPlayer;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

Expand All @@ -22,11 +27,11 @@
*
*/
// https://www.baeldung.com/spring-5-webclient
@Slf4j
public class KumiteWebclientServer implements IKumiteServer {
WebClient webClient;

public KumiteWebclientServer(Environment env) {

String serverUrl = env.getRequiredProperty("kumite.server.base-url");
String accessToken = env.getRequiredProperty("kumite.server.access_token");

Expand All @@ -36,6 +41,18 @@ public KumiteWebclientServer(Environment env) {
.build();
}

// https://github.com/spring-projects/spring-boot/issues/5077
public KumiteWebclientServer(Environment env, int randomServerPort) {
String serverUrl = env.getRequiredProperty("kumite.server.base-url")
.replaceFirst("LocalServerPort", Integer.toString(randomServerPort));
String accessToken = env.getRequiredProperty("kumite.server.access_token");

webClient = WebClient.builder()
.baseUrl(serverUrl)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.build();
}

@Override
public Flux<GameMetadata> searchGames(GameSearchParameters search) {
RequestHeadersSpec<?> spec = webClient.get()
Expand All @@ -45,27 +62,92 @@ public Flux<GameMetadata> searchGames(GameSearchParameters search) {
.build());

return spec.exchangeToFlux(r -> {
if (!r.statusCode().is2xxSuccessful()) {
throw new IllegalArgumentException("Request rejected: " + r.statusCode());
}
log.info("Search for games: {}", r.statusCode());
return r.bodyToFlux(GameMetadata.class);
});
}

@Override
public Flux<ContestMetadataRaw> searchContests(ContestSearchParameters contestSearchParameters) {
return webClient.get().uri("/api/contests").exchangeToFlux(r -> {
public Flux<ContestMetadataRaw> searchContests(ContestSearchParameters search) {
RequestHeadersSpec<?> spec = webClient.get()
.uri(uriBuilder -> uriBuilder.path("/api/contests")
.queryParamIfPresent("game_id", search.getGameId())
.queryParamIfPresent("contest_id", search.getContestId())
.build());

return spec.exchangeToFlux(r -> {
if (!r.statusCode().is2xxSuccessful()) {
throw new IllegalArgumentException("Request rejected: " + r.statusCode());
}
log.info("Search for contests: {}", r.statusCode());
return r.bodyToFlux(ContestMetadataRaw.class);
});
}

@Override
public Mono<ContestView> loadBoard(UUID contestId, UUID playerId) {
return webClient.get().uri("/api/board").exchangeToMono(r -> {
public Mono<ContestView> loadBoard(UUID playerId, UUID contestId) {
RequestHeadersSpec<?> spec = webClient.get()
.uri(uriBuilder -> uriBuilder.path("/api/board")
.queryParam("player_id", playerId)
.queryParam("contest_id", contestId)
.build());

return spec.exchangeToMono(r -> {
if (!r.statusCode().is2xxSuccessful()) {
throw new IllegalArgumentException("Request rejected: " + r.statusCode());
}
return r.bodyToMono(ContestView.class);
});
}

@Override
public Mono<ContestView> joinContest(UUID playerId, UUID contestId) {
return webClient.post().uri("/api/board/player").bodyValue(contestId).exchangeToMono(r -> {
public Mono<PlayingPlayer> joinContest(UUID playerId, UUID contestId) {
RequestBodySpec spec = webClient.post()
.uri(uriBuilder -> uriBuilder.path("/api/board/player")
.queryParam("player_id", playerId)
.queryParam("contest_id", contestId)
.build());

return spec.bodyValue(contestId).exchangeToMono(r -> {
if (!r.statusCode().is2xxSuccessful()) {
throw new IllegalArgumentException("Request rejected: " + r.statusCode());
}
return r.bodyToMono(PlayingPlayer.class);
});
}

@Override
public Mono<PlayerRawMovesHolder> getExampleMoves(UUID playerId, UUID contestId) {
RequestHeadersSpec<?> spec = webClient.get()
.uri(uriBuilder -> uriBuilder.path("/api/board/moves")
.queryParam("player_id", playerId)
.queryParam("contest_id", contestId)
.build());

return spec.exchangeToMono(r -> {
if (!r.statusCode().is2xxSuccessful()) {
throw new IllegalArgumentException("Request rejected: " + r.statusCode());
}
log.info("Search for moves: {}", r.statusCode());
return r.bodyToMono(PlayerRawMovesHolder.class);
});
}

@Override
public Mono<ContestView> playMove(UUID playerId, UUID contestId, Map<String, ?> move) {
RequestBodySpec spec = webClient.post()
.uri(uriBuilder -> uriBuilder.path("/api/board/move")
.queryParam("player_id", playerId)
.queryParam("contest_id", contestId)
.build());

return spec.bodyValue(move).exchangeToMono(r -> {
if (!r.statusCode().is2xxSuccessful()) {
throw new IllegalArgumentException("Request rejected: " + r.statusCode());
}
return r.bodyToMono(ContestView.class);
});
}
Expand Down
3 changes: 2 additions & 1 deletion player/src/main/resources/application-fake_player.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
kumite.playerId: 11111111-1111-1111-1111-111111111111
kumite.server:
access_token: someAccessTokenForFakePlayer
# See FakePlayerTokens in server module to regenerate this
access_token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMTExMTExMS0xMTExLTExMTEtMTExMS0xMTExMTExMTExMTIiLCJhdWQiOiJLdW1pdGUtU2VydmVyIiwibmJmIjoxNzI2MDYyNTc3LCJtYWluUGxheWVySWQiOiIxMTExMTExMS0xMTExLTExMTEtMTExMS0xMTExMTExMTExMTEiLCJpc3MiOiJodHRwczovL2t1bWl0ZS5jb20iLCJleHAiOjE3NTc1OTg1NzcsImlhdCI6MTcyNjA2MjU3NywianRpIjoiYmIyMGI0NWYtZDRkOS00MTM4LWJkOTMtY2I3OTliMzk3MGJlIn0.u0Vq43b_Vztoy0I-0mgILHi8yoHwQvcKcq9kyKoqWVM'
22 changes: 13 additions & 9 deletions public/src/main/java/eu/solven/kumite/contest/ContestView.java
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
package eu.solven.kumite.contest;

import java.util.Map;
import java.util.UUID;

import eu.solven.kumite.board.IKumiteBoardView;
import eu.solven.kumite.player.PlayingPlayer;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;

/**
* A snapshot of the Contest
*
* @author Benoit Lacelle
*
*/
@Value
@Builder
@Jacksonized
public class ContestView {
@NonNull
UUID contestId;
UUID playerId;

ContestDynamicMetadata dynamicMetadata;

IKumiteBoardView board;

@NonNull
Boolean playerHasJoined;
PlayingPlayer playingPlayer;

@NonNull
Boolean playerCanJoin;
ContestDynamicMetadata dynamicMetadata;

// Could be turned into a IKumiteBoardView by an IGame
@NonNull
Boolean accountIsViewing;
Map<String, ?> board;
}
Loading

0 comments on commit 01dc300

Please sign in to comment.