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