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());
58 WebTestClient.ResponseSpec responseSpec = client
60 .uri("/{chatroomId}/list", chatroomId)
61 .accept(MediaType.APPLICATION_JSON)
65 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
70 @DisplayName("Assert expected problem-details for unknown chatroom on GET /get/{chatroomId}")
71 void testUnknownChatroomExceptionForGetChatroom(@Autowired WebTestClient client)
74 UUID chatroomId = getRandomIdForOwnedShard();
75 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
78 WebTestClient.ResponseSpec responseSpec = client
80 .uri("/{chatroomId}", chatroomId)
81 .accept(MediaType.APPLICATION_JSON)
85 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
89 @DisplayName("Assert expected problem-details for unknown chatroom on PUT /put/{chatroomId}/{username}/{messageId}")
90 void testUnknownChatroomExceptionForPutMessage(@Autowired WebTestClient client)
93 UUID chatroomId = getRandomIdForOwnedShard();
94 String username = "foo";
96 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
99 WebTestClient.ResponseSpec responseSpec = client
102 "/{chatroomId}/{username}/{messageId}",
107 .accept(MediaType.APPLICATION_JSON)
111 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
115 @DisplayName("Assert expected problem-details for unknown chatroom on GET /get/{chatroomId}/{username}/{messageId}")
116 void testUnknownChatroomExceptionForGetMessage(@Autowired WebTestClient client)
119 UUID chatroomId = getRandomIdForOwnedShard();
120 String username = "foo";
121 Long messageId = 66l;
122 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
125 WebTestClient.ResponseSpec responseSpec = client
128 "/{chatroomId}/{username}/{messageId}",
132 .accept(MediaType.APPLICATION_JSON)
136 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
140 @DisplayName("Assert expected problem-details for unknown chatroom on GET /listen/{chatroomId}")
141 void testUnknownChatroomExceptionForListenChatroom(@Autowired WebTestClient client)
144 UUID chatroomId = getRandomIdForOwnedShard();
145 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
148 WebTestClient.ResponseSpec responseSpec = client
150 .uri("/{chatroomId}/listen", chatroomId)
151 // .accept(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON) << TODO: Does not work!
155 assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
158 private void assertProblemDetailsForUnknownChatroomException(
159 WebTestClient.ResponseSpec responseSpec,
163 .expectStatus().isNotFound()
165 .jsonPath("$.type").isEqualTo("/problem/unknown-chatroom")
166 .jsonPath("$.chatroomId").isEqualTo(chatroomId.toString());
170 @DisplayName("Assert expected problem-details for message mutation on PUT /put/{chatroomId}/{username}/{messageId}")
171 void testMessageMutationException(@Autowired WebTestClient client)
174 UUID chatroomId = getRandomIdForOwnedShard();
176 Long messageId = 66l;
177 Message.MessageKey key = Message.MessageKey.of(user, messageId);
178 Long serialNumberExistingMessage = 0l;
179 String timeExistingMessageAsString = "2023-01-09T20:44:57.389665447";
180 LocalDateTime timeExistingMessage = LocalDateTime.parse(timeExistingMessageAsString);
181 String textExistingMessage = "Existing";
182 String textMutatedMessage = "Mutated!";
183 ChatRoom chatRoom = new ChatRoom(
187 Clock.systemDefaultZone(),
189 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.just(chatRoom));
190 Message existingMessage = new Message(
192 serialNumberExistingMessage,
194 textExistingMessage);
195 when(chatRoomService.getMessage(any(Message.MessageKey.class)))
196 .thenReturn(Mono.just(existingMessage));
197 // Needed for readable error-reports, in case of a bug that leads to according unwanted call
198 when(chatRoomService.persistMessage(any(Message.MessageKey.class), any(LocalDateTime.class), any(String.class)))
199 .thenReturn(Mono.just(mock(Message.class)));
205 "/{chatroomId}/{username}/{messageId}",
209 .bodyValue(textMutatedMessage)
210 .accept(MediaType.APPLICATION_JSON)
213 .expectStatus().is4xxClientError()
215 .jsonPath("$.type").isEqualTo("/problem/message-mutation")
216 .jsonPath("$.existingMessage.id").isEqualTo(messageId)
217 .jsonPath("$.existingMessage.serial").isEqualTo(serialNumberExistingMessage)
218 .jsonPath("$.existingMessage.time").isEqualTo(timeExistingMessageAsString)
219 .jsonPath("$.existingMessage.user").isEqualTo(user)
220 .jsonPath("$.existingMessage.text").isEqualTo(textExistingMessage)
221 .jsonPath("$.mutatedText").isEqualTo(textMutatedMessage);
222 verify(chatRoomService, never()).persistMessage(eq(key), any(LocalDateTime.class), any(String.class));
226 @DisplayName("Assert expected problem-details for invalid username on PUT /put/{chatroomId}/{username}/{messageId}")
227 void testInvalidUsernameException(@Autowired WebTestClient client)
230 UUID chatroomId = getRandomIdForOwnedShard();
232 Long messageId = 66l;
233 Message.MessageKey key = Message.MessageKey.of(user, messageId);
234 String textMessage = "Hallo Welt";
235 ChatRoom chatRoom = new ChatRoom(
239 Clock.systemDefaultZone(),
241 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class)))
242 .thenReturn(Mono.just(chatRoom));
243 when(chatRoomService.getMessage(any(Message.MessageKey.class)))
244 .thenReturn(Mono.empty());
245 // Needed for readable error-reports, in case of a bug that leads to according unwanted call
246 when(chatRoomService.persistMessage(any(Message.MessageKey.class), any(LocalDateTime.class), any(String.class)))
247 .thenReturn(Mono.just(mock(Message.class)));
253 "/{chatroomId}/{username}/{messageId}",
257 .bodyValue(textMessage)
258 .accept(MediaType.APPLICATION_JSON)
261 .expectStatus().is4xxClientError()
263 .jsonPath("$.type").isEqualTo("/problem/invalid-username")
264 .jsonPath("$.username").isEqualTo(user);
265 verify(chatRoomService, never()).persistMessage(eq(key), any(LocalDateTime.class), any(String.class));
269 @DisplayName("Assert expected problem-details for not owned shard on GET /{chatroomId}")
270 void testShardNotOwnedExceptionForGetChatroom(@Autowired WebTestClient client)
273 UUID chatroomId = getRandomIdForForeignShard();
276 WebTestClient.ResponseSpec responseSpec = client
278 .uri("/{chatroomId}", chatroomId)
279 .accept(MediaType.APPLICATION_JSON)
283 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
287 @DisplayName("Assert expected problem-details for not owned shard on GET /list/{chatroomId}")
288 void testShardNotOwnedExceptionForListChatroom(@Autowired WebTestClient client)
291 UUID chatroomId = getRandomIdForForeignShard();
294 WebTestClient.ResponseSpec responseSpec = client
296 .uri("/{chatroomId}/list", chatroomId)
297 .accept(MediaType.APPLICATION_JSON)
301 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
305 @DisplayName("Assert expected problem-details for now owned shard on PUT /put/{chatroomId}/{username}/{messageId}")
306 void testShardNotOwnedExceptionForPutMessage(@Autowired WebTestClient client)
309 UUID chatroomId = getRandomIdForForeignShard();
310 String username = "foo";
311 Long messageId = 66l;
312 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
315 WebTestClient.ResponseSpec responseSpec = client
318 "/{chatroomId}/{username}/{messageId}",
323 .accept(MediaType.APPLICATION_JSON)
327 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
331 @DisplayName("Assert expected problem-details for not owned shard on GET /get/{chatroomId}/{username}/{messageId}")
332 void testShardNotOwnedExceptionForGetMessage(@Autowired WebTestClient client)
335 UUID chatroomId = getRandomIdForForeignShard();
336 String username = "foo";
337 Long messageId = 66l;
338 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
341 WebTestClient.ResponseSpec responseSpec = client
344 "/{chatroomId}/{username}/{messageId}",
348 .accept(MediaType.APPLICATION_JSON)
352 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
356 @DisplayName("Assert expected problem-details for not owned shard on GET /listen/{chatroomId}")
357 void testShardNotOwnedExceptionForListenChatroom(@Autowired WebTestClient client)
360 UUID chatroomId = getRandomIdForForeignShard();
361 when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
364 WebTestClient.ResponseSpec responseSpec = client
366 .uri("/{chatroomId}/listen", chatroomId)
367 // .accept(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON) << TODO: Does not work!
371 assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
374 private void assertProblemDetailsForShardNotOwnedException(
375 WebTestClient.ResponseSpec responseSpec,
379 .expectStatus().isNotFound()
381 .jsonPath("$.type").isEqualTo("/problem/shard-not-owned")
382 .jsonPath("$.shard").isEqualTo(shard);
385 private UUID getRandomIdForOwnedShard()
387 Set<Integer> ownedShards = ownedShards();
392 randomId = UUID.randomUUID();
394 while (!ownedShards.contains(shardingStrategy.selectShard(randomId)));
399 private UUID getRandomIdForForeignShard()
401 Set<Integer> ownedShards = ownedShards();
406 randomId = UUID.randomUUID();
408 while (ownedShards.contains(shardingStrategy.selectShard(randomId)));
413 private Set<Integer> ownedShards()
416 .of(properties.getInmemory().getOwnedShards())
417 .mapToObj(shard -> Integer.valueOf(shard))
418 .collect(Collectors.toSet());