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 de.juplo.kafka.chat.backend.persistence.inmemory.ShardingStrategy;
7 import lombok.extern.slf4j.Slf4j;
8 import org.junit.jupiter.api.DisplayName;
9 import org.junit.jupiter.api.Test;
10 import org.springframework.beans.factory.annotation.Autowired;
11 import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
12 import org.springframework.boot.test.context.SpringBootTest;
13 import org.springframework.boot.test.mock.mockito.MockBean;
14 import org.springframework.http.MediaType;
15 import org.springframework.test.web.reactive.server.WebTestClient;
16 import reactor.core.publisher.Mono;
18 import java.time.Clock;
19 import java.time.LocalDateTime;
21 import java.util.UUID;
22 import java.util.stream.Collectors;
23 import java.util.stream.IntStream;
25 import static org.mockito.ArgumentMatchers.any;
26 import static org.mockito.Mockito.*;
29 @SpringBootTest(properties = {
30 "spring.main.allow-bean-definition-overriding=true",
31 "chat.backend.inmemory.sharding-strategy=kafkalike",
32 "chat.backend.inmemory.num-shards=10",
33 "chat.backend.inmemory.owned-shards=6",
35 @AutoConfigureWebTestClient
37 public class ChatBackendControllerTest
40 ChatBackendProperties properties;
42 ShardingStrategy shardingStrategy;
45 InMemoryChatHomeService chatHomeService;
47 ChatRoomService chatRoomService;
50 @DisplayName("Assert expected problem-details for unknown chatroom on GET /list/{chatroomId}")
51 void testUnknownChatroomExceptionForListChatroom(@Autowired WebTestClient client)
54 UUID chatroomId = getRandomIdForOwnedShard();
55 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
56 when(chatHomeService.getOwnedShards()).thenReturn(new int[] { 6 });
59 WebTestClient.ResponseSpec responseSpec = client
61 .uri("/{chatroomId}/list", chatroomId)
62 .accept(MediaType.APPLICATION_JSON)
66 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
71 @DisplayName("Assert expected problem-details for unknown chatroom on GET /get/{chatroomId}")
72 void testUnknownChatroomExceptionForGetChatroom(@Autowired WebTestClient client)
75 UUID chatroomId = getRandomIdForOwnedShard();
76 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
77 when(chatHomeService.getOwnedShards()).thenReturn(new int[] { 6 });
80 WebTestClient.ResponseSpec responseSpec = client
82 .uri("/{chatroomId}", chatroomId)
83 .accept(MediaType.APPLICATION_JSON)
87 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
91 @DisplayName("Assert expected problem-details for unknown chatroom on PUT /put/{chatroomId}/{username}/{messageId}")
92 void testUnknownChatroomExceptionForPutMessage(@Autowired WebTestClient client)
95 UUID chatroomId = getRandomIdForOwnedShard();
96 String username = "foo";
98 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
99 when(chatHomeService.getOwnedShards()).thenReturn(new int[] { 6 });
102 WebTestClient.ResponseSpec responseSpec = client
105 "/{chatroomId}/{username}/{messageId}",
110 .accept(MediaType.APPLICATION_JSON)
114 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
118 @DisplayName("Assert expected problem-details for unknown chatroom on GET /get/{chatroomId}/{username}/{messageId}")
119 void testUnknownChatroomExceptionForGetMessage(@Autowired WebTestClient client)
122 UUID chatroomId = getRandomIdForOwnedShard();
123 String username = "foo";
124 Long messageId = 66l;
125 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
126 when(chatHomeService.getOwnedShards()).thenReturn(new int[] { 6 });
129 WebTestClient.ResponseSpec responseSpec = client
132 "/{chatroomId}/{username}/{messageId}",
136 .accept(MediaType.APPLICATION_JSON)
140 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
144 @DisplayName("Assert expected problem-details for unknown chatroom on GET /listen/{chatroomId}")
145 void testUnknownChatroomExceptionForListenChatroom(@Autowired WebTestClient client)
148 UUID chatroomId = getRandomIdForOwnedShard();
149 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
150 when(chatHomeService.getOwnedShards()).thenReturn(new int[] { 6 });
153 WebTestClient.ResponseSpec responseSpec = client
155 .uri("/{chatroomId}/listen", chatroomId)
156 // .accept(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON) << TODO: Does not work!
160 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
163 private void assertProblemDetailsForUnknownChatroomException(
164 WebTestClient.ResponseSpec responseSpec,
168 .expectStatus().isNotFound()
170 .jsonPath("$.type").isEqualTo("/problem/unknown-chatroom")
171 .jsonPath("$.chatroomId").isEqualTo(chatroomId.toString());
175 @DisplayName("Assert expected problem-details for message mutation on PUT /put/{chatroomId}/{username}/{messageId}")
176 void testMessageMutationException(@Autowired WebTestClient client)
179 UUID chatroomId = getRandomIdForOwnedShard();
181 Long messageId = 66l;
182 Message.MessageKey key = Message.MessageKey.of(user, messageId);
183 Long serialNumberExistingMessage = 0l;
184 String timeExistingMessageAsString = "2023-01-09T20:44:57.389665447";
185 LocalDateTime timeExistingMessage = LocalDateTime.parse(timeExistingMessageAsString);
186 String textExistingMessage = "Existing";
187 String textMutatedMessage = "Mutated!";
188 ChatRoom chatRoom = new ChatRoom(
192 Clock.systemDefaultZone(),
194 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.just(chatRoom));
195 Message existingMessage = new Message(
197 serialNumberExistingMessage,
199 textExistingMessage);
200 when(chatRoomService.getMessage(any(Message.MessageKey.class)))
201 .thenReturn(Mono.just(existingMessage));
202 // Needed for readable error-reports, in case of a bug that leads to according unwanted call
203 when(chatRoomService.persistMessage(any(Message.MessageKey.class), any(LocalDateTime.class), any(String.class)))
204 .thenReturn(Mono.just(mock(Message.class)));
210 "/{chatroomId}/{username}/{messageId}",
214 .bodyValue(textMutatedMessage)
215 .accept(MediaType.APPLICATION_JSON)
218 .expectStatus().is4xxClientError()
220 .jsonPath("$.type").isEqualTo("/problem/message-mutation")
221 .jsonPath("$.existingMessage.id").isEqualTo(messageId)
222 .jsonPath("$.existingMessage.serial").isEqualTo(serialNumberExistingMessage)
223 .jsonPath("$.existingMessage.time").isEqualTo(timeExistingMessageAsString)
224 .jsonPath("$.existingMessage.user").isEqualTo(user)
225 .jsonPath("$.existingMessage.text").isEqualTo(textExistingMessage)
226 .jsonPath("$.mutatedText").isEqualTo(textMutatedMessage);
227 verify(chatRoomService, never()).persistMessage(eq(key), any(LocalDateTime.class), any(String.class));
231 @DisplayName("Assert expected problem-details for invalid username on PUT /put/{chatroomId}/{username}/{messageId}")
232 void testInvalidUsernameException(@Autowired WebTestClient client)
235 UUID chatroomId = getRandomIdForOwnedShard();
237 Long messageId = 66l;
238 Message.MessageKey key = Message.MessageKey.of(user, messageId);
239 String textMessage = "Hallo Welt";
240 ChatRoom chatRoom = new ChatRoom(
244 Clock.systemDefaultZone(),
246 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class)))
247 .thenReturn(Mono.just(chatRoom));
248 when(chatRoomService.getMessage(any(Message.MessageKey.class)))
249 .thenReturn(Mono.empty());
250 // Needed for readable error-reports, in case of a bug that leads to according unwanted call
251 when(chatRoomService.persistMessage(any(Message.MessageKey.class), any(LocalDateTime.class), any(String.class)))
252 .thenReturn(Mono.just(mock(Message.class)));
258 "/{chatroomId}/{username}/{messageId}",
262 .bodyValue(textMessage)
263 .accept(MediaType.APPLICATION_JSON)
266 .expectStatus().is4xxClientError()
268 .jsonPath("$.type").isEqualTo("/problem/invalid-username")
269 .jsonPath("$.username").isEqualTo(user);
270 verify(chatRoomService, never()).persistMessage(eq(key), any(LocalDateTime.class), any(String.class));
274 @DisplayName("Assert expected problem-details for not owned shard on GET /{chatroomId}")
275 void testShardNotOwnedExceptionForGetChatroom(@Autowired WebTestClient client)
278 UUID chatroomId = getRandomIdForForeignShard();
281 WebTestClient.ResponseSpec responseSpec = client
283 .uri("/{chatroomId}", chatroomId)
284 .accept(MediaType.APPLICATION_JSON)
288 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
292 @DisplayName("Assert expected problem-details for not owned shard on GET /list/{chatroomId}")
293 void testShardNotOwnedExceptionForListChatroom(@Autowired WebTestClient client)
296 UUID chatroomId = getRandomIdForForeignShard();
299 WebTestClient.ResponseSpec responseSpec = client
301 .uri("/{chatroomId}/list", chatroomId)
302 .accept(MediaType.APPLICATION_JSON)
306 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
310 @DisplayName("Assert expected problem-details for now owned shard on PUT /put/{chatroomId}/{username}/{messageId}")
311 void testShardNotOwnedExceptionForPutMessage(@Autowired WebTestClient client)
314 UUID chatroomId = getRandomIdForForeignShard();
315 String username = "foo";
316 Long messageId = 66l;
317 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
320 WebTestClient.ResponseSpec responseSpec = client
323 "/{chatroomId}/{username}/{messageId}",
328 .accept(MediaType.APPLICATION_JSON)
332 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
336 @DisplayName("Assert expected problem-details for not owned shard on GET /get/{chatroomId}/{username}/{messageId}")
337 void testShardNotOwnedExceptionForGetMessage(@Autowired WebTestClient client)
340 UUID chatroomId = getRandomIdForForeignShard();
341 String username = "foo";
342 Long messageId = 66l;
343 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
346 WebTestClient.ResponseSpec responseSpec = client
349 "/{chatroomId}/{username}/{messageId}",
353 .accept(MediaType.APPLICATION_JSON)
357 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
361 @DisplayName("Assert expected problem-details for not owned shard on GET /listen/{chatroomId}")
362 void testShardNotOwnedExceptionForListenChatroom(@Autowired WebTestClient client)
365 UUID chatroomId = getRandomIdForForeignShard();
366 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
369 WebTestClient.ResponseSpec responseSpec = client
371 .uri("/{chatroomId}/listen", chatroomId)
372 // .accept(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON) << TODO: Does not work!
376 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
379 private void assertProblemDetailsForShardNotOwnedException(
380 WebTestClient.ResponseSpec responseSpec,
384 .expectStatus().isNotFound()
386 .jsonPath("$.type").isEqualTo("/problem/shard-not-owned")
387 .jsonPath("$.shard").isEqualTo(shard);
390 private UUID getRandomIdForOwnedShard()
392 Set<Integer> ownedShards = ownedShards();
397 randomId = UUID.randomUUID();
399 while (!ownedShards.contains(shardingStrategy.selectShard(randomId)));
404 private UUID getRandomIdForForeignShard()
406 Set<Integer> ownedShards = ownedShards();
411 randomId = UUID.randomUUID();
413 while (ownedShards.contains(shardingStrategy.selectShard(randomId)));
418 private Set<Integer> ownedShards()
421 .of(properties.getInmemory().getOwnedShards())
422 .mapToObj(shard -> Integer.valueOf(shard))
423 .collect(Collectors.toSet());