fix: GREEN - Fixed NPE in `ShardedChatHome.getChatRoom()` for foreign shard
[demos/kafka/chat] / src / test / java / de / juplo / kafka / chat / backend / api / ChatBackendControllerTest.java
1 package de.juplo.kafka.chat.backend.api;
2
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;
16
17 import java.time.Clock;
18 import java.time.LocalDateTime;
19 import java.util.Set;
20 import java.util.UUID;
21 import java.util.stream.Collectors;
22 import java.util.stream.IntStream;
23
24 import static org.mockito.ArgumentMatchers.any;
25 import static org.mockito.Mockito.*;
26
27
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",
33     })
34 @AutoConfigureWebTestClient
35 @Slf4j
36 public class ChatBackendControllerTest
37 {
38   @Autowired
39   ChatBackendProperties properties;
40   @Autowired
41   ShardingStrategy shardingStrategy;
42
43   @MockBean
44   InMemoryChatHomeService chatHomeService;
45   @MockBean
46   ChatRoomService chatRoomService;
47
48   @Test
49   @DisplayName("Assert expected problem-details for unknown chatroom on GET /list/{chatroomId}")
50   void testUnknownChatroomExceptionForListChatroom(@Autowired WebTestClient client)
51   {
52     // Given
53     UUID chatroomId = getRandomIdForOwnedShard();
54     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
55
56     // When
57     WebTestClient.ResponseSpec responseSpec = client
58         .get()
59         .uri("/{chatroomId}/list", chatroomId)
60         .accept(MediaType.APPLICATION_JSON)
61         .exchange();
62
63     // Then
64     assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
65   }
66
67
68   @Test
69   @DisplayName("Assert expected problem-details for unknown chatroom on GET /get/{chatroomId}")
70   void testUnknownChatroomExceptionForGetChatroom(@Autowired WebTestClient client)
71   {
72     // Given
73     UUID chatroomId = getRandomIdForOwnedShard();
74     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
75
76     // When
77     WebTestClient.ResponseSpec responseSpec = client
78         .get()
79         .uri("/{chatroomId}", chatroomId)
80         .accept(MediaType.APPLICATION_JSON)
81         .exchange();
82
83     // Then
84     assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
85   }
86
87   @Test
88   @DisplayName("Assert expected problem-details for unknown chatroom on PUT /put/{chatroomId}/{username}/{messageId}")
89   void testUnknownChatroomExceptionForPutMessage(@Autowired WebTestClient client)
90   {
91     // Given
92     UUID chatroomId = getRandomIdForOwnedShard();
93     String username = "foo";
94     Long messageId = 66l;
95     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
96
97     // When
98     WebTestClient.ResponseSpec responseSpec = client
99         .put()
100         .uri(
101             "/{chatroomId}/{username}/{messageId}",
102             chatroomId,
103             username,
104             messageId)
105         .bodyValue("bar")
106         .accept(MediaType.APPLICATION_JSON)
107         .exchange();
108
109     // Then
110     assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
111   }
112
113   @Test
114   @DisplayName("Assert expected problem-details for unknown chatroom on GET /get/{chatroomId}/{username}/{messageId}")
115   void testUnknownChatroomExceptionForGetMessage(@Autowired WebTestClient client)
116   {
117     // Given
118     UUID chatroomId = getRandomIdForOwnedShard();
119     String username = "foo";
120     Long messageId = 66l;
121     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
122
123     // When
124     WebTestClient.ResponseSpec responseSpec = client
125         .get()
126         .uri(
127             "/{chatroomId}/{username}/{messageId}",
128             chatroomId,
129             username,
130             messageId)
131         .accept(MediaType.APPLICATION_JSON)
132         .exchange();
133
134     // Then
135     assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
136   }
137
138   @Test
139   @DisplayName("Assert expected problem-details for unknown chatroom on GET /listen/{chatroomId}")
140   void testUnknownChatroomExceptionForListenChatroom(@Autowired WebTestClient client)
141   {
142     // Given
143     UUID chatroomId = getRandomIdForOwnedShard();
144     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
145
146     // When
147     WebTestClient.ResponseSpec responseSpec = client
148         .get()
149         .uri("/{chatroomId}/listen", chatroomId)
150         // .accept(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON) << TODO: Does not work!
151         .exchange();
152
153     // Then
154     assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
155   }
156
157   private void assertProblemDetailsForUnknownChatroomException(
158       WebTestClient.ResponseSpec responseSpec,
159       UUID chatroomId)
160   {
161     responseSpec
162         .expectStatus().isNotFound()
163         .expectBody()
164         .jsonPath("$.type").isEqualTo("/problem/unknown-chatroom")
165         .jsonPath("$.chatroomId").isEqualTo(chatroomId.toString());
166   }
167
168   @Test
169   @DisplayName("Assert expected problem-details for message mutation on PUT /put/{chatroomId}/{username}/{messageId}")
170   void testMessageMutationException(@Autowired WebTestClient client)
171   {
172     // Given
173     UUID chatroomId = getRandomIdForOwnedShard();
174     String user = "foo";
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(
183         chatroomId,
184         "Test-ChatRoom",
185         0,
186         Clock.systemDefaultZone(),
187         chatRoomService, 8);
188     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.just(chatRoom));
189     Message existingMessage = new Message(
190         key,
191         serialNumberExistingMessage,
192         timeExistingMessage,
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)));
199
200     // When
201     client
202         .put()
203         .uri(
204             "/{chatroomId}/{username}/{messageId}",
205             chatroomId,
206             user,
207             messageId)
208         .bodyValue(textMutatedMessage)
209         .accept(MediaType.APPLICATION_JSON)
210         .exchange()
211         // Then
212         .expectStatus().is4xxClientError()
213         .expectBody()
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));
222   }
223
224   @Test
225   @DisplayName("Assert expected problem-details for invalid username on PUT /put/{chatroomId}/{username}/{messageId}")
226   void testInvalidUsernameException(@Autowired WebTestClient client)
227   {
228     // Given
229     UUID chatroomId = getRandomIdForOwnedShard();
230     String user = "Foo";
231     Long messageId = 66l;
232     Message.MessageKey key = Message.MessageKey.of(user, messageId);
233     String textMessage = "Hallo Welt";
234     ChatRoom chatRoom = new ChatRoom(
235         chatroomId,
236         "Test-ChatRoom",
237         0,
238         Clock.systemDefaultZone(),
239         chatRoomService, 8);
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)));
247
248     // When
249     client
250         .put()
251         .uri(
252             "/{chatroomId}/{username}/{messageId}",
253             chatroomId,
254             user,
255             messageId)
256         .bodyValue(textMessage)
257         .accept(MediaType.APPLICATION_JSON)
258         .exchange()
259         // Then
260         .expectStatus().is4xxClientError()
261         .expectBody()
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));
265   }
266
267   @Test
268   @DisplayName("Assert expected problem-details for not owned shard on GET /{chatroomId}")
269   void testShardNotOwnedExceptionForGetChatroom(@Autowired WebTestClient client)
270   {
271     // Given
272     UUID chatroomId = getRandomIdForForeignShard();
273
274     // When
275     WebTestClient.ResponseSpec responseSpec = client
276         .get()
277         .uri("/{chatroomId}", chatroomId)
278         .accept(MediaType.APPLICATION_JSON)
279         .exchange();
280
281     // Then
282     assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
283   }
284
285   @Test
286   @DisplayName("Assert expected problem-details for not owned shard on GET /list/{chatroomId}")
287   void testShardNotOwnedExceptionForListChatroom(@Autowired WebTestClient client)
288   {
289     // Given
290     UUID chatroomId = getRandomIdForForeignShard();
291
292     // When
293     WebTestClient.ResponseSpec responseSpec = client
294         .get()
295         .uri("/{chatroomId}/list", chatroomId)
296         .accept(MediaType.APPLICATION_JSON)
297         .exchange();
298
299     // Then
300     assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
301   }
302
303   @Test
304   @DisplayName("Assert expected problem-details for now owned shard on PUT /put/{chatroomId}/{username}/{messageId}")
305   void testShardNotOwnedExceptionForPutMessage(@Autowired WebTestClient client)
306   {
307     // Given
308     UUID chatroomId = getRandomIdForForeignShard();
309     String username = "foo";
310     Long messageId = 66l;
311     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
312
313     // When
314     WebTestClient.ResponseSpec responseSpec = client
315         .put()
316         .uri(
317             "/{chatroomId}/{username}/{messageId}",
318             chatroomId,
319             username,
320             messageId)
321         .bodyValue("bar")
322         .accept(MediaType.APPLICATION_JSON)
323         .exchange();
324
325     // Then
326     assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
327   }
328
329   @Test
330   @DisplayName("Assert expected problem-details for not owned shard on GET /get/{chatroomId}/{username}/{messageId}")
331   void testShardNotOwnedExceptionForGetMessage(@Autowired WebTestClient client)
332   {
333     // Given
334     UUID chatroomId = getRandomIdForForeignShard();
335     String username = "foo";
336     Long messageId = 66l;
337     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
338
339     // When
340     WebTestClient.ResponseSpec responseSpec = client
341         .get()
342         .uri(
343             "/{chatroomId}/{username}/{messageId}",
344             chatroomId,
345             username,
346             messageId)
347         .accept(MediaType.APPLICATION_JSON)
348         .exchange();
349
350     // Then
351     assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
352   }
353
354   @Test
355   @DisplayName("Assert expected problem-details for not owned shard on GET /listen/{chatroomId}")
356   void testShardNotOwnedExceptionForListenChatroom(@Autowired WebTestClient client)
357   {
358     // Given
359     UUID chatroomId = getRandomIdForForeignShard();
360     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
361
362     // When
363     WebTestClient.ResponseSpec responseSpec = client
364         .get()
365         .uri("/{chatroomId}/listen", chatroomId)
366         // .accept(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON) << TODO: Does not work!
367         .exchange();
368
369     // Then
370     assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
371   }
372
373   private void assertProblemDetailsForShardNotOwnedException(
374       WebTestClient.ResponseSpec responseSpec,
375       int shard)
376   {
377     responseSpec
378         .expectStatus().isNotFound()
379         .expectBody()
380         .jsonPath("$.type").isEqualTo("/problem/shard-not-owned")
381         .jsonPath("$.shard").isEqualTo(shard);
382   }
383
384   private UUID getRandomIdForOwnedShard()
385   {
386     Set<Integer> ownedShards = ownedShards();
387     UUID randomId;
388
389     do
390     {
391       randomId = UUID.randomUUID();
392     }
393     while (!ownedShards.contains(shardingStrategy.selectShard(randomId)));
394
395     return randomId;
396   }
397
398   private UUID getRandomIdForForeignShard()
399   {
400     Set<Integer> ownedShards = ownedShards();
401     UUID randomId;
402
403     do
404     {
405       randomId = UUID.randomUUID();
406     }
407     while (ownedShards.contains(shardingStrategy.selectShard(randomId)));
408
409     return randomId;
410   }
411
412   private Set<Integer> ownedShards()
413   {
414     return IntStream
415         .of(properties.getInmemory().getOwnedShards())
416         .mapToObj(shard -> Integer.valueOf(shard))
417         .collect(Collectors.toSet());
418   }
419 }