1 package de.juplo.kafka.chat.backend.api;
3 import de.juplo.kafka.chat.backend.ChatBackendProperties;
4 import de.juplo.kafka.chat.backend.domain.*;
5 import de.juplo.kafka.chat.backend.persistence.inmemory.InMemoryChatHomeService;
6 import lombok.extern.slf4j.Slf4j;
7 import org.junit.jupiter.api.DisplayName;
8 import org.junit.jupiter.api.Test;
9 import org.springframework.beans.factory.annotation.Autowired;
10 import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
11 import org.springframework.boot.test.context.SpringBootTest;
12 import org.springframework.boot.test.mock.mockito.MockBean;
13 import org.springframework.http.MediaType;
14 import org.springframework.test.web.reactive.server.WebTestClient;
15 import reactor.core.publisher.Mono;
17 import java.time.Clock;
18 import java.time.LocalDateTime;
20 import java.util.UUID;
21 import java.util.stream.Collectors;
22 import java.util.stream.IntStream;
24 import static org.mockito.ArgumentMatchers.any;
25 import static org.mockito.Mockito.*;
28 @SpringBootTest(properties = {
29 "spring.main.allow-bean-definition-overriding=true",
30 "chat.backend.inmemory.sharding-strategy=kafkalike",
31 "chat.backend.inmemory.num-shards=10",
32 "chat.backend.inmemory.owned-shards=6",
34 @AutoConfigureWebTestClient
36 public class ChatBackendControllerTest
39 ChatBackendProperties properties;
41 ShardingStrategy shardingStrategy;
44 InMemoryChatHomeService chatHomeService;
46 ChatRoomService chatRoomService;
49 @DisplayName("Assert expected problem-details for unknown chatroom on GET /list/{chatroomId}")
50 void testUnknownChatroomExceptionForListChatroom(@Autowired WebTestClient client)
53 UUID chatroomId = getRandomIdForOwnedShard();
54 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
57 WebTestClient.ResponseSpec responseSpec = client
59 .uri("/{chatroomId}/list", chatroomId)
60 .accept(MediaType.APPLICATION_JSON)
64 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
69 @DisplayName("Assert expected problem-details for unknown chatroom on GET /get/{chatroomId}")
70 void testUnknownChatroomExceptionForGetChatroom(@Autowired WebTestClient client)
73 UUID chatroomId = getRandomIdForOwnedShard();
74 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
77 WebTestClient.ResponseSpec responseSpec = client
79 .uri("/{chatroomId}", chatroomId)
80 .accept(MediaType.APPLICATION_JSON)
84 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
88 @DisplayName("Assert expected problem-details for unknown chatroom on PUT /put/{chatroomId}/{username}/{messageId}")
89 void testUnknownChatroomExceptionForPutMessage(@Autowired WebTestClient client)
92 UUID chatroomId = getRandomIdForOwnedShard();
93 String username = "foo";
95 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
98 WebTestClient.ResponseSpec responseSpec = client
101 "/{chatroomId}/{username}/{messageId}",
106 .accept(MediaType.APPLICATION_JSON)
110 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
114 @DisplayName("Assert expected problem-details for unknown chatroom on GET /get/{chatroomId}/{username}/{messageId}")
115 void testUnknownChatroomExceptionForGetMessage(@Autowired WebTestClient client)
118 UUID chatroomId = getRandomIdForOwnedShard();
119 String username = "foo";
120 Long messageId = 66l;
121 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
124 WebTestClient.ResponseSpec responseSpec = client
127 "/{chatroomId}/{username}/{messageId}",
131 .accept(MediaType.APPLICATION_JSON)
135 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
139 @DisplayName("Assert expected problem-details for unknown chatroom on GET /listen/{chatroomId}")
140 void testUnknownChatroomExceptionForListenChatroom(@Autowired WebTestClient client)
143 UUID chatroomId = getRandomIdForOwnedShard();
144 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
147 WebTestClient.ResponseSpec responseSpec = client
149 .uri("/{chatroomId}/listen", chatroomId)
150 // .accept(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON) << TODO: Does not work!
154 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
157 private void assertProblemDetailsForUnknownChatroomException(
158 WebTestClient.ResponseSpec responseSpec,
162 .expectStatus().isNotFound()
164 .jsonPath("$.type").isEqualTo("/problem/unknown-chatroom")
165 .jsonPath("$.chatroomId").isEqualTo(chatroomId.toString());
169 @DisplayName("Assert expected problem-details for message mutation on PUT /put/{chatroomId}/{username}/{messageId}")
170 void testMessageMutationException(@Autowired WebTestClient client)
173 UUID chatroomId = getRandomIdForOwnedShard();
175 Long messageId = 66l;
176 Message.MessageKey key = Message.MessageKey.of(user, messageId);
177 Long serialNumberExistingMessage = 0l;
178 String timeExistingMessageAsString = "2023-01-09T20:44:57.389665447";
179 LocalDateTime timeExistingMessage = LocalDateTime.parse(timeExistingMessageAsString);
180 String textExistingMessage = "Existing";
181 String textMutatedMessage = "Mutated!";
182 ChatRoom chatRoom = new ChatRoom(
186 Clock.systemDefaultZone(),
188 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.just(chatRoom));
189 Message existingMessage = new Message(
191 serialNumberExistingMessage,
193 textExistingMessage);
194 when(chatRoomService.getMessage(any(Message.MessageKey.class)))
195 .thenReturn(Mono.just(existingMessage));
196 // Needed for readable error-reports, in case of a bug that leads to according unwanted call
197 when(chatRoomService.persistMessage(any(Message.MessageKey.class), any(LocalDateTime.class), any(String.class)))
198 .thenReturn(Mono.just(mock(Message.class)));
204 "/{chatroomId}/{username}/{messageId}",
208 .bodyValue(textMutatedMessage)
209 .accept(MediaType.APPLICATION_JSON)
212 .expectStatus().is4xxClientError()
214 .jsonPath("$.type").isEqualTo("/problem/message-mutation")
215 .jsonPath("$.existingMessage.id").isEqualTo(messageId)
216 .jsonPath("$.existingMessage.serial").isEqualTo(serialNumberExistingMessage)
217 .jsonPath("$.existingMessage.time").isEqualTo(timeExistingMessageAsString)
218 .jsonPath("$.existingMessage.user").isEqualTo(user)
219 .jsonPath("$.existingMessage.text").isEqualTo(textExistingMessage)
220 .jsonPath("$.mutatedText").isEqualTo(textMutatedMessage);
221 verify(chatRoomService, never()).persistMessage(eq(key), any(LocalDateTime.class), any(String.class));
225 @DisplayName("Assert expected problem-details for invalid username on PUT /put/{chatroomId}/{username}/{messageId}")
226 void testInvalidUsernameException(@Autowired WebTestClient client)
229 UUID chatroomId = getRandomIdForOwnedShard();
231 Long messageId = 66l;
232 Message.MessageKey key = Message.MessageKey.of(user, messageId);
233 String textMessage = "Hallo Welt";
234 ChatRoom chatRoom = new ChatRoom(
238 Clock.systemDefaultZone(),
240 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class)))
241 .thenReturn(Mono.just(chatRoom));
242 when(chatRoomService.getMessage(any(Message.MessageKey.class)))
243 .thenReturn(Mono.empty());
244 // Needed for readable error-reports, in case of a bug that leads to according unwanted call
245 when(chatRoomService.persistMessage(any(Message.MessageKey.class), any(LocalDateTime.class), any(String.class)))
246 .thenReturn(Mono.just(mock(Message.class)));
252 "/{chatroomId}/{username}/{messageId}",
256 .bodyValue(textMessage)
257 .accept(MediaType.APPLICATION_JSON)
260 .expectStatus().is4xxClientError()
262 .jsonPath("$.type").isEqualTo("/problem/invalid-username")
263 .jsonPath("$.username").isEqualTo(user);
264 verify(chatRoomService, never()).persistMessage(eq(key), any(LocalDateTime.class), any(String.class));
268 @DisplayName("Assert expected problem-details for not owned shard on GET /{chatroomId}")
269 void testShardNotOwnedExceptionForGetChatroom(@Autowired WebTestClient client)
272 UUID chatroomId = getRandomIdForForeignShard();
275 WebTestClient.ResponseSpec responseSpec = client
277 .uri("/{chatroomId}", chatroomId)
278 .accept(MediaType.APPLICATION_JSON)
282 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
286 @DisplayName("Assert expected problem-details for not owned shard on GET /list/{chatroomId}")
287 void testShardNotOwnedExceptionForListChatroom(@Autowired WebTestClient client)
290 UUID chatroomId = getRandomIdForForeignShard();
293 WebTestClient.ResponseSpec responseSpec = client
295 .uri("/{chatroomId}/list", chatroomId)
296 .accept(MediaType.APPLICATION_JSON)
300 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
304 @DisplayName("Assert expected problem-details for now owned shard on PUT /put/{chatroomId}/{username}/{messageId}")
305 void testShardNotOwnedExceptionForPutMessage(@Autowired WebTestClient client)
308 UUID chatroomId = getRandomIdForForeignShard();
309 String username = "foo";
310 Long messageId = 66l;
311 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
314 WebTestClient.ResponseSpec responseSpec = client
317 "/{chatroomId}/{username}/{messageId}",
322 .accept(MediaType.APPLICATION_JSON)
326 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
330 @DisplayName("Assert expected problem-details for not owned shard on GET /get/{chatroomId}/{username}/{messageId}")
331 void testShardNotOwnedExceptionForGetMessage(@Autowired WebTestClient client)
334 UUID chatroomId = getRandomIdForForeignShard();
335 String username = "foo";
336 Long messageId = 66l;
337 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
340 WebTestClient.ResponseSpec responseSpec = client
343 "/{chatroomId}/{username}/{messageId}",
347 .accept(MediaType.APPLICATION_JSON)
351 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
355 @DisplayName("Assert expected problem-details for not owned shard on GET /listen/{chatroomId}")
356 void testShardNotOwnedExceptionForListenChatroom(@Autowired WebTestClient client)
359 UUID chatroomId = getRandomIdForForeignShard();
360 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
363 WebTestClient.ResponseSpec responseSpec = client
365 .uri("/{chatroomId}/listen", chatroomId)
366 // .accept(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON) << TODO: Does not work!
370 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
373 private void assertProblemDetailsForShardNotOwnedException(
374 WebTestClient.ResponseSpec responseSpec,
378 .expectStatus().isNotFound()
380 .jsonPath("$.type").isEqualTo("/problem/shard-not-owned")
381 .jsonPath("$.shard").isEqualTo(shard);
384 private UUID getRandomIdForOwnedShard()
386 Set<Integer> ownedShards = ownedShards();
391 randomId = UUID.randomUUID();
393 while (!ownedShards.contains(shardingStrategy.selectShard(randomId)));
398 private UUID getRandomIdForForeignShard()
400 Set<Integer> ownedShards = ownedShards();
405 randomId = UUID.randomUUID();
407 while (ownedShards.contains(shardingStrategy.selectShard(randomId)));
412 private Set<Integer> ownedShards()
415 .of(properties.getInmemory().getOwnedShards())
416 .mapToObj(shard -> Integer.valueOf(shard))
417 .collect(Collectors.toSet());