Skip to content

Commit 01dc300

Browse files
committed
Add an integration test going through the API
1 parent 7d67742 commit 01dc300

31 files changed

+850
-185
lines changed

monolith/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
<version>${project.version}</version>
2323
</dependency>
2424

25+
<dependency>
26+
<groupId>org.springframework.boot</groupId>
27+
<artifactId>spring-boot-starter-test</artifactId>
28+
<scope>test</scope>
29+
</dependency>
2530
</dependencies>
2631

2732
<build>

monolith/src/main/resources/application.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ spring:
77
- "default_player"
88
- "default_server"
99
- "server"
10-
- "fake_player"
10+
- "fake_player"
11+
- inject_default_games
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package eu.solven.kumite.app.it;
2+
3+
import java.util.Map;
4+
import java.util.Optional;
5+
import java.util.UUID;
6+
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.ExtendWith;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.boot.test.context.SpringBootTest;
11+
import org.springframework.boot.test.web.server.LocalServerPort;
12+
import org.springframework.core.env.Environment;
13+
import org.springframework.test.context.ActiveProfiles;
14+
import org.springframework.test.context.TestPropertySource;
15+
import org.springframework.test.context.junit.jupiter.SpringExtension;
16+
17+
import eu.solven.kumite.app.IKumiteServer;
18+
import eu.solven.kumite.app.IKumiteSpringProfiles;
19+
import eu.solven.kumite.app.KumiteServerApplication;
20+
import eu.solven.kumite.app.KumiteWebclientServer;
21+
import eu.solven.kumite.contest.ContestSearchParameters;
22+
import eu.solven.kumite.game.GameSearchParameters;
23+
import eu.solven.kumite.player.PlayerRawMovesHolder;
24+
import lombok.extern.slf4j.Slf4j;
25+
import reactor.core.publisher.Mono;
26+
27+
/**
28+
* This integration-test serves 2 purposes: first it shows how one can chain call to play a game: it can help ensure the
29+
* API is stable and simple; second, it ensures the API is actually functional (e.g. up to serializibility of involved
30+
* classes).
31+
*
32+
* @author Benoit Lacelle
33+
* @see 'TestTSPLifecycle'
34+
*/
35+
// Should this move to `monolith` module?
36+
@ExtendWith(SpringExtension.class)
37+
@SpringBootTest(classes = KumiteServerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
38+
@ActiveProfiles({ IKumiteSpringProfiles.P_DEFAULT, IKumiteSpringProfiles.P_DEFAULT_FAKE_PLAYER, })
39+
@TestPropertySource(properties = { "kumite.random.seed=123",
40+
"kumite.server.base-url=http://localhost:LocalServerPort",
41+
"kumite.random.seed=123" })
42+
@Slf4j
43+
public class TestTSPLifecycleThroughRouter {
44+
45+
// https://stackoverflow.com/questions/30312058/spring-boot-how-to-get-the-running-port
46+
@LocalServerPort
47+
int randomServerPort;
48+
49+
@Autowired
50+
Environment env;
51+
52+
@Test
53+
public void testSinglePlayer() {
54+
IKumiteServer kumiteServer = new KumiteWebclientServer(env, randomServerPort);
55+
56+
UUID playerId = env.getRequiredProperty("kumite.playerId", UUID.class);
57+
58+
kumiteServer
59+
// Search for games given a human-friendly pattern
60+
.searchGames(GameSearchParameters.builder().titleRegex(Optional.of(".*Salesman.*")).build())
61+
// Search for contest
62+
.flatMap(game -> {
63+
log.info("Processing game={}", game);
64+
return kumiteServer.searchContests(
65+
ContestSearchParameters.builder().gameId(Optional.of(game.getGameId())).build());
66+
})
67+
// Filter relevant contests
68+
.filter(c -> {
69+
// log.info("c={}", c);
70+
return true;
71+
})
72+
.filter(c -> c.getDynamicMetadata().isAcceptingPlayers())
73+
.filter(c -> !c.getDynamicMetadata().isGameOver())
74+
// Join each relevant contest
75+
.flatMap(contest -> {
76+
log.info("Joining contest={}", contest);
77+
return kumiteServer.joinContest(playerId, contest.getContestId()).flatMap(playerPlayer -> {
78+
log.info("playerPlayer={}", playerPlayer);
79+
80+
return kumiteServer.loadBoard(playerId, contest.getContestId()).flatMap(joinedContest -> {
81+
log.info("Received board for contest={}", joinedContest.getContestId());
82+
83+
Mono<PlayerRawMovesHolder> exampleMoves =
84+
kumiteServer.getExampleMoves(playerId, joinedContest.getContestId());
85+
86+
return exampleMoves.flatMap(moves -> {
87+
Optional<Map<String, ?>> someMove = moves.getMoves().values().stream().findAny();
88+
return Mono.justOrEmpty(someMove);
89+
}).flatMap(move -> {
90+
return kumiteServer.playMove(playerId, joinedContest.getContestId(), move);
91+
});
92+
});
93+
});
94+
})
95+
96+
.doOnError(t -> {
97+
log.error("Something went wrong", t);
98+
})
99+
.then()
100+
.block();
101+
}
102+
}

player/src/main/java/eu/solven/kumite/app/IKumiteServer.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package eu.solven.kumite.app;
22

3+
import java.util.Map;
34
import java.util.UUID;
45

56
import eu.solven.kumite.contest.ContestMetadataRaw;
67
import eu.solven.kumite.contest.ContestSearchParameters;
78
import eu.solven.kumite.contest.ContestView;
89
import eu.solven.kumite.game.GameMetadata;
910
import eu.solven.kumite.game.GameSearchParameters;
11+
import eu.solven.kumite.player.PlayerRawMovesHolder;
12+
import eu.solven.kumite.player.PlayingPlayer;
1013
import reactor.core.publisher.Flux;
1114
import reactor.core.publisher.Mono;
1215

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

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

20-
Mono<ContestView> joinContest(UUID playerId, UUID contestId);
23+
Mono<PlayingPlayer> joinContest(UUID playerId, UUID contestId);
24+
25+
Mono<PlayerRawMovesHolder> getExampleMoves(UUID playerId, UUID contestId);
26+
27+
Mono<ContestView> playMove(UUID playerId, UUID contestId, Map<String, ?> move);
2128

2229
}

player/src/main/java/eu/solven/kumite/app/KumitePlayerComponentsConfiguration.java

+10-7
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ public IKumiteServer kumiteServer(Environment env) {
3838

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

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

67-
if (contestView.getPlayerHasJoined()) {
65+
if (contestView.getPlayingPlayer().isPlayerHasJoined()) {
66+
log.info("Received board for already joined contestId={}", contestId);
67+
6868
playingContests.add(contestId);
6969
contestToDetails.put(contestId, contestView);
70-
} else if (contestView.getPlayerCanJoin()) {
71-
kumiteServer.joinContest(playerId, contestId);
70+
} else if (contestView.getPlayingPlayer().isPlayerCanJoin()) {
71+
log.info("Received board for joinable contestId={}", contestId);
72+
kumiteServer.joinContest(playerId, contestId).subscribe(view -> {
73+
log.info("Received board for joined contestId={}", contestId);
74+
contestToDetails.put(contestId, contestView);
75+
});
7276
}
73-
7477
})
7578
.subscribe(view -> {
7679
log.info("View: {}", view);

player/src/main/java/eu/solven/kumite/app/KumiteWebclientServer.java

+89-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
package eu.solven.kumite.app;
22

3+
import java.util.Map;
34
import java.util.UUID;
45

56
import org.springframework.core.env.Environment;
67
import org.springframework.http.HttpHeaders;
78
import org.springframework.web.reactive.function.client.WebClient;
9+
import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec;
810
import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec;
911

1012
import eu.solven.kumite.contest.ContestMetadataRaw;
1113
import eu.solven.kumite.contest.ContestSearchParameters;
1214
import eu.solven.kumite.contest.ContestView;
1315
import eu.solven.kumite.game.GameMetadata;
1416
import eu.solven.kumite.game.GameSearchParameters;
17+
import eu.solven.kumite.player.PlayerRawMovesHolder;
18+
import eu.solven.kumite.player.PlayingPlayer;
19+
import lombok.extern.slf4j.Slf4j;
1520
import reactor.core.publisher.Flux;
1621
import reactor.core.publisher.Mono;
1722

@@ -22,11 +27,11 @@
2227
*
2328
*/
2429
// https://www.baeldung.com/spring-5-webclient
30+
@Slf4j
2531
public class KumiteWebclientServer implements IKumiteServer {
2632
WebClient webClient;
2733

2834
public KumiteWebclientServer(Environment env) {
29-
3035
String serverUrl = env.getRequiredProperty("kumite.server.base-url");
3136
String accessToken = env.getRequiredProperty("kumite.server.access_token");
3237

@@ -36,6 +41,18 @@ public KumiteWebclientServer(Environment env) {
3641
.build();
3742
}
3843

44+
// https://github.com/spring-projects/spring-boot/issues/5077
45+
public KumiteWebclientServer(Environment env, int randomServerPort) {
46+
String serverUrl = env.getRequiredProperty("kumite.server.base-url")
47+
.replaceFirst("LocalServerPort", Integer.toString(randomServerPort));
48+
String accessToken = env.getRequiredProperty("kumite.server.access_token");
49+
50+
webClient = WebClient.builder()
51+
.baseUrl(serverUrl)
52+
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
53+
.build();
54+
}
55+
3956
@Override
4057
public Flux<GameMetadata> searchGames(GameSearchParameters search) {
4158
RequestHeadersSpec<?> spec = webClient.get()
@@ -45,27 +62,92 @@ public Flux<GameMetadata> searchGames(GameSearchParameters search) {
4562
.build());
4663

4764
return spec.exchangeToFlux(r -> {
65+
if (!r.statusCode().is2xxSuccessful()) {
66+
throw new IllegalArgumentException("Request rejected: " + r.statusCode());
67+
}
68+
log.info("Search for games: {}", r.statusCode());
4869
return r.bodyToFlux(GameMetadata.class);
4970
});
5071
}
5172

5273
@Override
53-
public Flux<ContestMetadataRaw> searchContests(ContestSearchParameters contestSearchParameters) {
54-
return webClient.get().uri("/api/contests").exchangeToFlux(r -> {
74+
public Flux<ContestMetadataRaw> searchContests(ContestSearchParameters search) {
75+
RequestHeadersSpec<?> spec = webClient.get()
76+
.uri(uriBuilder -> uriBuilder.path("/api/contests")
77+
.queryParamIfPresent("game_id", search.getGameId())
78+
.queryParamIfPresent("contest_id", search.getContestId())
79+
.build());
80+
81+
return spec.exchangeToFlux(r -> {
82+
if (!r.statusCode().is2xxSuccessful()) {
83+
throw new IllegalArgumentException("Request rejected: " + r.statusCode());
84+
}
85+
log.info("Search for contests: {}", r.statusCode());
5586
return r.bodyToFlux(ContestMetadataRaw.class);
5687
});
5788
}
5889

5990
@Override
60-
public Mono<ContestView> loadBoard(UUID contestId, UUID playerId) {
61-
return webClient.get().uri("/api/board").exchangeToMono(r -> {
91+
public Mono<ContestView> loadBoard(UUID playerId, UUID contestId) {
92+
RequestHeadersSpec<?> spec = webClient.get()
93+
.uri(uriBuilder -> uriBuilder.path("/api/board")
94+
.queryParam("player_id", playerId)
95+
.queryParam("contest_id", contestId)
96+
.build());
97+
98+
return spec.exchangeToMono(r -> {
99+
if (!r.statusCode().is2xxSuccessful()) {
100+
throw new IllegalArgumentException("Request rejected: " + r.statusCode());
101+
}
62102
return r.bodyToMono(ContestView.class);
63103
});
64104
}
65105

66106
@Override
67-
public Mono<ContestView> joinContest(UUID playerId, UUID contestId) {
68-
return webClient.post().uri("/api/board/player").bodyValue(contestId).exchangeToMono(r -> {
107+
public Mono<PlayingPlayer> joinContest(UUID playerId, UUID contestId) {
108+
RequestBodySpec spec = webClient.post()
109+
.uri(uriBuilder -> uriBuilder.path("/api/board/player")
110+
.queryParam("player_id", playerId)
111+
.queryParam("contest_id", contestId)
112+
.build());
113+
114+
return spec.bodyValue(contestId).exchangeToMono(r -> {
115+
if (!r.statusCode().is2xxSuccessful()) {
116+
throw new IllegalArgumentException("Request rejected: " + r.statusCode());
117+
}
118+
return r.bodyToMono(PlayingPlayer.class);
119+
});
120+
}
121+
122+
@Override
123+
public Mono<PlayerRawMovesHolder> getExampleMoves(UUID playerId, UUID contestId) {
124+
RequestHeadersSpec<?> spec = webClient.get()
125+
.uri(uriBuilder -> uriBuilder.path("/api/board/moves")
126+
.queryParam("player_id", playerId)
127+
.queryParam("contest_id", contestId)
128+
.build());
129+
130+
return spec.exchangeToMono(r -> {
131+
if (!r.statusCode().is2xxSuccessful()) {
132+
throw new IllegalArgumentException("Request rejected: " + r.statusCode());
133+
}
134+
log.info("Search for moves: {}", r.statusCode());
135+
return r.bodyToMono(PlayerRawMovesHolder.class);
136+
});
137+
}
138+
139+
@Override
140+
public Mono<ContestView> playMove(UUID playerId, UUID contestId, Map<String, ?> move) {
141+
RequestBodySpec spec = webClient.post()
142+
.uri(uriBuilder -> uriBuilder.path("/api/board/move")
143+
.queryParam("player_id", playerId)
144+
.queryParam("contest_id", contestId)
145+
.build());
146+
147+
return spec.bodyValue(move).exchangeToMono(r -> {
148+
if (!r.statusCode().is2xxSuccessful()) {
149+
throw new IllegalArgumentException("Request rejected: " + r.statusCode());
150+
}
69151
return r.bodyToMono(ContestView.class);
70152
});
71153
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
kumite.playerId: 11111111-1111-1111-1111-111111111111
22
kumite.server:
3-
access_token: someAccessTokenForFakePlayer
3+
# See FakePlayerTokens in server module to regenerate this
4+
access_token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMTExMTExMS0xMTExLTExMTEtMTExMS0xMTExMTExMTExMTIiLCJhdWQiOiJLdW1pdGUtU2VydmVyIiwibmJmIjoxNzI2MDYyNTc3LCJtYWluUGxheWVySWQiOiIxMTExMTExMS0xMTExLTExMTEtMTExMS0xMTExMTExMTExMTEiLCJpc3MiOiJodHRwczovL2t1bWl0ZS5jb20iLCJleHAiOjE3NTc1OTg1NzcsImlhdCI6MTcyNjA2MjU3NywianRpIjoiYmIyMGI0NWYtZDRkOS00MTM4LWJkOTMtY2I3OTliMzk3MGJlIn0.u0Vq43b_Vztoy0I-0mgILHi8yoHwQvcKcq9kyKoqWVM'
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
11
package eu.solven.kumite.contest;
22

3+
import java.util.Map;
34
import java.util.UUID;
45

5-
import eu.solven.kumite.board.IKumiteBoardView;
6+
import eu.solven.kumite.player.PlayingPlayer;
67
import lombok.Builder;
78
import lombok.NonNull;
89
import lombok.Value;
910
import lombok.extern.jackson.Jacksonized;
1011

12+
/**
13+
* A snapshot of the Contest
14+
*
15+
* @author Benoit Lacelle
16+
*
17+
*/
1118
@Value
1219
@Builder
1320
@Jacksonized
1421
public class ContestView {
22+
@NonNull
1523
UUID contestId;
16-
UUID playerId;
17-
18-
ContestDynamicMetadata dynamicMetadata;
19-
20-
IKumiteBoardView board;
2124

2225
@NonNull
23-
Boolean playerHasJoined;
26+
PlayingPlayer playingPlayer;
2427

2528
@NonNull
26-
Boolean playerCanJoin;
29+
ContestDynamicMetadata dynamicMetadata;
2730

31+
// Could be turned into a IKumiteBoardView by an IGame
2832
@NonNull
29-
Boolean accountIsViewing;
33+
Map<String, ?> board;
3034
}

0 commit comments

Comments
 (0)