32b1e3578a02206b670e8d049231fee0aa92bad3
[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     when(chatHomeService.getOwnedShards()).thenReturn(new int[] { 6 });
57
58     // When
59     WebTestClient.ResponseSpec responseSpec = client
60         .get()
61         .uri("/{chatroomId}/list", chatroomId)
62         .accept(MediaType.APPLICATION_JSON)
63         .exchange();
64
65     // Then
66     assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
67   }
68
69
70   @Test
71   @DisplayName("Assert expected problem-details for unknown chatroom on GET /get/{chatroomId}")
72   void testUnknownChatroomExceptionForGetChatroom(@Autowired WebTestClient client)
73   {
74     // Given
75     UUID chatroomId = getRandomIdForOwnedShard();
76     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
77     when(chatHomeService.getOwnedShards()).thenReturn(new int[] { 6 });
78
79     // When
80     WebTestClient.ResponseSpec responseSpec = client
81         .get()
82         .uri("/{chatroomId}", chatroomId)
83         .accept(MediaType.APPLICATION_JSON)
84         .exchange();
85
86     // Then
87     assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
88   }
89
90   @Test
91   @DisplayName("Assert expected problem-details for unknown chatroom on PUT /put/{chatroomId}/{username}/{messageId}")
92   void testUnknownChatroomExceptionForPutMessage(@Autowired WebTestClient client)
93   {
94     // Given
95     UUID chatroomId = getRandomIdForOwnedShard();
96     String username = "foo";
97     Long messageId = 66l;
98     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
99     when(chatHomeService.getOwnedShards()).thenReturn(new int[] { 6 });
100
101     // When
102     WebTestClient.ResponseSpec responseSpec = client
103         .put()
104         .uri(
105             "/{chatroomId}/{username}/{messageId}",
106             chatroomId,
107             username,
108             messageId)
109         .bodyValue("bar")
110         .accept(MediaType.APPLICATION_JSON)
111         .exchange();
112
113     // Then
114     assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
115   }
116
117   @Test
118   @DisplayName("Assert expected problem-details for unknown chatroom on GET /get/{chatroomId}/{username}/{messageId}")
119   void testUnknownChatroomExceptionForGetMessage(@Autowired WebTestClient client)
120   {
121     // Given
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 });
127
128     // When
129     WebTestClient.ResponseSpec responseSpec = client
130         .get()
131         .uri(
132             "/{chatroomId}/{username}/{messageId}",
133             chatroomId,
134             username,
135             messageId)
136         .accept(MediaType.APPLICATION_JSON)
137         .exchange();
138
139     // Then
140     assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
141   }
142
143   @Test
144   @DisplayName("Assert expected problem-details for unknown chatroom on GET /listen/{chatroomId}")
145   void testUnknownChatroomExceptionForListenChatroom(@Autowired WebTestClient client)
146   {
147     // Given
148     UUID chatroomId = getRandomIdForOwnedShard();
149     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
150     when(chatHomeService.getOwnedShards()).thenReturn(new int[] { 6 });
151
152     // When
153     WebTestClient.ResponseSpec responseSpec = client
154         .get()
155         .uri("/{chatroomId}/listen", chatroomId)
156         // .accept(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON) << TODO: Does not work!
157         .exchange();
158
159     // Then
160     assertProblemDetailsForUnknownChatroomException(responseSpec, chatroomId);
161   }
162
163   private void assertProblemDetailsForUnknownChatroomException(
164       WebTestClient.ResponseSpec responseSpec,
165       UUID chatroomId)
166   {
167     responseSpec
168         .expectStatus().isNotFound()
169         .expectBody()
170         .jsonPath("$.type").isEqualTo("/problem/unknown-chatroom")
171         .jsonPath("$.chatroomId").isEqualTo(chatroomId.toString());
172   }
173
174   @Test
175   @DisplayName("Assert expected problem-details for message mutation on PUT /put/{chatroomId}/{username}/{messageId}")
176   void testMessageMutationException(@Autowired WebTestClient client)
177   {
178     // Given
179     UUID chatroomId = getRandomIdForOwnedShard();
180     String user = "foo";
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(
189         chatroomId,
190         "Test-ChatRoom",
191         0,
192         Clock.systemDefaultZone(),
193         chatRoomService, 8);
194     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.just(chatRoom));
195     Message existingMessage = new Message(
196         key,
197         serialNumberExistingMessage,
198         timeExistingMessage,
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)));
205
206     // When
207     client
208         .put()
209         .uri(
210             "/{chatroomId}/{username}/{messageId}",
211             chatroomId,
212             user,
213             messageId)
214         .bodyValue(textMutatedMessage)
215         .accept(MediaType.APPLICATION_JSON)
216         .exchange()
217         // Then
218         .expectStatus().is4xxClientError()
219         .expectBody()
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));
228   }
229
230   @Test
231   @DisplayName("Assert expected problem-details for invalid username on PUT /put/{chatroomId}/{username}/{messageId}")
232   void testInvalidUsernameException(@Autowired WebTestClient client)
233   {
234     // Given
235     UUID chatroomId = getRandomIdForOwnedShard();
236     String user = "Foo";
237     Long messageId = 66l;
238     Message.MessageKey key = Message.MessageKey.of(user, messageId);
239     String textMessage = "Hallo Welt";
240     ChatRoom chatRoom = new ChatRoom(
241         chatroomId,
242         "Test-ChatRoom",
243         0,
244         Clock.systemDefaultZone(),
245         chatRoomService, 8);
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)));
253
254     // When
255     client
256         .put()
257         .uri(
258             "/{chatroomId}/{username}/{messageId}",
259             chatroomId,
260             user,
261             messageId)
262         .bodyValue(textMessage)
263         .accept(MediaType.APPLICATION_JSON)
264         .exchange()
265         // Then
266         .expectStatus().is4xxClientError()
267         .expectBody()
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));
271   }
272
273   @Test
274   @DisplayName("Assert expected problem-details for not owned shard on GET /{chatroomId}")
275   void testShardNotOwnedExceptionForGetChatroom(@Autowired WebTestClient client)
276   {
277     // Given
278     UUID chatroomId = getRandomIdForForeignShard();
279
280     // When
281     WebTestClient.ResponseSpec responseSpec = client
282         .get()
283         .uri("/{chatroomId}", chatroomId)
284         .accept(MediaType.APPLICATION_JSON)
285         .exchange();
286
287     // Then
288     assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
289   }
290
291   @Test
292   @DisplayName("Assert expected problem-details for not owned shard on GET /list/{chatroomId}")
293   void testShardNotOwnedExceptionForListChatroom(@Autowired WebTestClient client)
294   {
295     // Given
296     UUID chatroomId = getRandomIdForForeignShard();
297
298     // When
299     WebTestClient.ResponseSpec responseSpec = client
300         .get()
301         .uri("/{chatroomId}/list", chatroomId)
302         .accept(MediaType.APPLICATION_JSON)
303         .exchange();
304
305     // Then
306     assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
307   }
308
309   @Test
310   @DisplayName("Assert expected problem-details for now owned shard on PUT /put/{chatroomId}/{username}/{messageId}")
311   void testShardNotOwnedExceptionForPutMessage(@Autowired WebTestClient client)
312   {
313     // Given
314     UUID chatroomId = getRandomIdForForeignShard();
315     String username = "foo";
316     Long messageId = 66l;
317     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
318
319     // When
320     WebTestClient.ResponseSpec responseSpec = client
321         .put()
322         .uri(
323             "/{chatroomId}/{username}/{messageId}",
324             chatroomId,
325             username,
326             messageId)
327         .bodyValue("bar")
328         .accept(MediaType.APPLICATION_JSON)
329         .exchange();
330
331     // Then
332     assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
333   }
334
335   @Test
336   @DisplayName("Assert expected problem-details for not owned shard on GET /get/{chatroomId}/{username}/{messageId}")
337   void testShardNotOwnedExceptionForGetMessage(@Autowired WebTestClient client)
338   {
339     // Given
340     UUID chatroomId = getRandomIdForForeignShard();
341     String username = "foo";
342     Long messageId = 66l;
343     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
344
345     // When
346     WebTestClient.ResponseSpec responseSpec = client
347         .get()
348         .uri(
349             "/{chatroomId}/{username}/{messageId}",
350             chatroomId,
351             username,
352             messageId)
353         .accept(MediaType.APPLICATION_JSON)
354         .exchange();
355
356     // Then
357     assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
358   }
359
360   @Test
361   @DisplayName("Assert expected problem-details for not owned shard on GET /listen/{chatroomId}")
362   void testShardNotOwnedExceptionForListenChatroom(@Autowired WebTestClient client)
363   {
364     // Given
365     UUID chatroomId = getRandomIdForForeignShard();
366     when(chatHomeService.getChatRoom(anyInt(), any(UUID.class))).thenReturn(Mono.empty());
367
368     // When
369     WebTestClient.ResponseSpec responseSpec = client
370         .get()
371         .uri("/{chatroomId}/listen", chatroomId)
372         // .accept(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_JSON) << TODO: Does not work!
373         .exchange();
374
375     // Then
376     assertProblemDetailsForShardNotOwnedException(responseSpec, shardingStrategy.selectShard(chatroomId));
377   }
378
379   private void assertProblemDetailsForShardNotOwnedException(
380       WebTestClient.ResponseSpec responseSpec,
381       int shard)
382   {
383     responseSpec
384         .expectStatus().isNotFound()
385         .expectBody()
386         .jsonPath("$.type").isEqualTo("/problem/shard-not-owned")
387         .jsonPath("$.shard").isEqualTo(shard);
388   }
389
390   private UUID getRandomIdForOwnedShard()
391   {
392     Set<Integer> ownedShards = ownedShards();
393     UUID randomId;
394
395     do
396     {
397       randomId = UUID.randomUUID();
398     }
399     while (!ownedShards.contains(shardingStrategy.selectShard(randomId)));
400
401     return randomId;
402   }
403
404   private UUID getRandomIdForForeignShard()
405   {
406     Set<Integer> ownedShards = ownedShards();
407     UUID randomId;
408
409     do
410     {
411       randomId = UUID.randomUUID();
412     }
413     while (ownedShards.contains(shardingStrategy.selectShard(randomId)));
414
415     return randomId;
416   }
417
418   private Set<Integer> ownedShards()
419   {
420     return IntStream
421         .of(properties.getInmemory().getOwnedShards())
422         .mapToObj(shard -> Integer.valueOf(shard))
423         .collect(Collectors.toSet());
424   }
425 }