diff --git a/service-chats/src/main/java/io/volvox/chats/Chat.java b/service-chats/src/main/java/io/volvox/chats/Chat.java index 1df5ef6..c8188c8 100644 --- a/service-chats/src/main/java/io/volvox/chats/Chat.java +++ b/service-chats/src/main/java/io/volvox/chats/Chat.java @@ -5,16 +5,22 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.quarkus.hibernate.reactive.panache.PanacheEntityBase; import io.smallrye.mutiny.Uni; +import java.util.SortedSet; import java.util.StringJoiner; +import java.util.TreeSet; import javax.persistence.Cacheable; +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Enumerated; +import javax.persistence.FetchType; import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; import javax.validation.constraints.Max; import javax.validation.constraints.Pattern; import javax.validation.constraints.Positive; -import javax.validation.constraints.Size; +import org.hibernate.annotations.SortNatural; @Entity @Cacheable @@ -27,18 +33,44 @@ public class Chat extends PanacheEntityBase { @JsonSerialize(using = ChatIdJsonSerializer.class) @JsonDeserialize(using = ChatIdJsonDeserializer.class) public Long id; + + /** + * null = unknown username + * "" = empty username + */ @Column(length = 128) public String name; - @Size(message = "Username length is not valid", min = 5) + + // Field definition and bounds + @OneToMany(orphanRemoval = true, mappedBy = "chat", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @OrderBy("time DESC") + @SortNatural + // Field serialization + @JsonIgnore + public SortedSet nameHistory = new TreeSet<>(); + + /** + * null = unknown username + * "" = empty username + */ @Column(length = 48) - @Size(message = "Username must not be an empty string", min = 1, max = 12 + 32) - @Pattern(message = "Username contains invalid characters", regexp = "^(?:[a-zA-Z\\d][_]?)+$") - @Pattern(message = "Username is not valid", regexp = "^(?:translation_|mv_)?[a-zA-Z]([a-zA-Z_\\d]){1,30}[a-zA-Z\\d]$") + @Pattern(message = "Username contains invalid characters", regexp = "^(?:|(?:[a-zA-Z\\d][_]?)+)$") + @Pattern(message = "Username is not valid", regexp = "^(?:|(?:translation_|mv_)?[a-zA-Z]([a-zA-Z_\\d]){1,30}[a-zA-Z\\d])$") public String username; + + // Field definition and bounds + @OneToMany(orphanRemoval = true, mappedBy = "chat", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @OrderBy("time DESC") + @SortNatural + // Field serialization + @JsonIgnore + public SortedSet usernameHistory = new TreeSet<>(); + @Enumerated public Status status; - @JsonIgnore + + @JsonIgnore public ChatId getChatId() { return ChatId.fromLong(id); } @@ -57,12 +89,12 @@ public class Chat extends PanacheEntityBase { .toString(); } - public static Uni findUsername(String username) { + public static Uni findUsername(String username) { if (username == null) { throw new NullPointerException("Username must not be null"); } else if (username.isBlank()) { throw new NullPointerException("Username must not be blank"); } - return find("from Chat where username = ?1", username).firstResult(); - } + return find("from Chat where username = ?1", username).firstResult(); + } } diff --git a/service-chats/src/main/java/io/volvox/chats/ChatService.java b/service-chats/src/main/java/io/volvox/chats/ChatService.java index e7cbc25..cea2b76 100644 --- a/service-chats/src/main/java/io/volvox/chats/ChatService.java +++ b/service-chats/src/main/java/io/volvox/chats/ChatService.java @@ -1,11 +1,11 @@ package io.volvox.chats; import io.quarkus.hibernate.reactive.panache.Panache; -import io.quarkus.hibernate.reactive.panache.PanacheEntityBase; import io.reactiverse.elasticsearch.client.mutiny.RestHighLevelClient; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import io.vertx.core.json.JsonObject; +import java.sql.Date; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -15,9 +15,8 @@ import javax.ws.rs.NotFoundException; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.get.GetRequest; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.QueryBuilders; @@ -30,11 +29,10 @@ public class ChatService { @Inject RestHighLevelClient restHighLevelClient; - private Uni updateIndex(Chat chat) { - var request = new IndexRequest("chats"); - request.id(ChatId.toString(chat.id)); - request.source(JsonObject.mapFrom(chat).toString(), XContentType.JSON); - return restHighLevelClient.indexAsync(request, RequestOptions.DEFAULT); + private Uni updateIndex(Chat chat) { + var request = new org.elasticsearch.action.update.UpdateRequest("chats", ChatId.toString(chat.id)) + .docAsUpsert(true).doc(JsonObject.mapFrom(chat).toString(), XContentType.JSON); + return restHighLevelClient.updateAsync(request, RequestOptions.DEFAULT); } private Uni removeFromIndex(Long id) { @@ -66,9 +64,17 @@ public class ChatService { } public Uni delete(Long id) { - return Panache.withTransaction(() -> Chat.findById(id) + HistoricChatName.deleteByChatId(id); + return Panache.withTransaction(() -> Chat.findById(id) .onItem().ifNull().failWith(NotFoundException::new) - .flatMap(PanacheEntityBase::delete) + .flatMap(entity -> { + var nameDelete = HistoricChatName.deleteByChatId(id); + var usernameDelete = HistoricChatUsername.deleteByChatId(id); + + var entityDelete = entity.delete(); + return Uni.combine().all().unis(nameDelete, usernameDelete).combinedWith((a, b) -> null) + .replaceWith(entityDelete); + }) .replaceWith(removeFromIndex(id)) .onItem().transform(DeleteResponse::status) .replaceWithVoid() @@ -79,31 +85,69 @@ public class ChatService { if (chat.id != null && id != null && !Objects.equals(chat.id, id)) { throw new IllegalArgumentException("Chat id is different than id"); } - // Find chat by id - return Panache.withTransaction(() -> Chat.findById(id) - .flatMap(entity -> { - if (entity == null) { - // Persist the chat if not found - return Chat.persist(chat) - // Return the chat - .replaceWith(chat); - } else { - // Update all fields - if (chat.name != null) { - entity.name = chat.name; - } - if (chat.username != null) { - entity.username = chat.username; - } - if (chat.status != null) { - entity.status = chat.status; - } - // Return the updated item - return Uni.createFrom().item(entity); - } - }) - // Update index - .onItem().transformToUni(updatedChat -> updateIndex(updatedChat).replaceWith(updatedChat)) + + return Panache.withTransaction(() -> { + var transactionTimestamp = new Date(System.currentTimeMillis()); + // Find chat by id + var oldChat = Chat.findById(id); + return oldChat + .flatMap(entity -> { + if (entity == null) { + // Persist the chat if not found + return chat.persist(); + } else { + // Update all fields + if (chat.name != null) { + entity.name = chat.name; + } + if (chat.username != null) { + entity.username = chat.username; + } + if (chat.status != null) { + entity.status = chat.status; + } + // Return the updated item + return Uni.createFrom().item(entity); + } + }) + .flatMap(updatedEntity -> { + // Update the username with history + var usernameUpdater = HistoricChatUsername.findNewest(updatedEntity.id).flatMap(newestUsername -> { + if (chat.username != null + && (newestUsername == null || !Objects.equals(newestUsername.username, chat.username))) { + updatedEntity.username = chat.username; + + var newUsername = new HistoricChatUsername(); + newUsername.chat = updatedEntity; + newUsername.username = chat.username; + newUsername.time = transactionTimestamp; + return newUsername.persist().replaceWithVoid(); + } else { + return Uni.createFrom().voidItem(); + } + }); + + // Update the name with history + var nameUpdater = HistoricChatName.findNewest(updatedEntity.id).flatMap(newestName -> { + if (chat.name != null + && (newestName == null || !Objects.equals(newestName.name, chat.name))) { + updatedEntity.name = chat.name; + + var newName = new HistoricChatName(); + newName.chat = updatedEntity; + newName.name = chat.name; + newName.time = transactionTimestamp; + return newName.persist().replaceWithVoid(); + } else { + return Uni.createFrom().voidItem(); + } + }); + + return nameUpdater.replaceWith(usernameUpdater).replaceWith(updatedEntity); + }) + // Update index + .chain(updatedChat -> updateIndex(updatedChat).replaceWith(updatedChat)); + } ); } @@ -120,7 +164,7 @@ public class ChatService { } private Uni> search(String term, String match) { - SearchRequest searchRequest = new SearchRequest("chats"); + SearchRequest searchRequest = new SearchRequest("chats"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.matchQuery(term, match)); searchRequest.source(searchSourceBuilder); diff --git a/service-chats/src/main/java/io/volvox/chats/HistoricChatName.java b/service-chats/src/main/java/io/volvox/chats/HistoricChatName.java new file mode 100644 index 0000000..8283ecd --- /dev/null +++ b/service-chats/src/main/java/io/volvox/chats/HistoricChatName.java @@ -0,0 +1,66 @@ +package io.volvox.chats; + +import io.quarkus.hibernate.reactive.panache.PanacheEntityBase; +import io.quarkus.panache.common.Sort; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import java.sql.Date; +import java.util.StringJoiner; +import javax.persistence.Cacheable; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +@Entity(name = "chat_name") +@Cacheable +public class HistoricChatName extends PanacheEntityBase implements Comparable { + @Id + @GeneratedValue + public Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = true) + @JoinColumn(name = "chat_id", referencedColumnName = "id", nullable = true) + Chat chat; + + // Field definition and bounds + @Column(name = "time", nullable = false) + public Date time; + + // Field definition and bounds + @Column(length = 128) + public String name; + + public static Uni findNewest(Long chatId) { + if (chatId == null) { + throw new NullPointerException("Id must not be null"); + } + return find("from chat_name where chat_id = ?1", Sort.by("time").descending(), chatId).firstResult(); + } + + public static Multi listAll(Long chatId) { + if (chatId == null) { + throw new NullPointerException("Id must not be null"); + } + return find("from chat_name where chat_id = ?1", Sort.by("time").ascending(), chatId).stream(); + } + + public static Uni deleteByChatId(Long chatId) { + return delete("from chat_name where chat_id = ?1", chatId); + } + + @Override public int compareTo(HistoricChatName o) { + return o.time.compareTo(this.time); + } + + @Override public String toString() { + return new StringJoiner(", ", HistoricChatName.class.getSimpleName() + "[", "]") + .add("id=" + id) + .add("time=" + time) + .add("name='" + name + "'") + .toString(); + } +} diff --git a/service-chats/src/main/java/io/volvox/chats/HistoricChatUsername.java b/service-chats/src/main/java/io/volvox/chats/HistoricChatUsername.java new file mode 100644 index 0000000..37d227c --- /dev/null +++ b/service-chats/src/main/java/io/volvox/chats/HistoricChatUsername.java @@ -0,0 +1,73 @@ +package io.volvox.chats; + +import io.quarkus.hibernate.reactive.panache.PanacheEntityBase; +import io.quarkus.panache.common.Sort; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import java.sql.Date; +import java.util.StringJoiner; +import javax.persistence.Cacheable; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +@Entity(name = "chat_username") +@Cacheable +public class HistoricChatUsername extends PanacheEntityBase implements Comparable { + @Id + @GeneratedValue + public Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = true) + @JoinColumn(name = "chat_id", referencedColumnName = "id", nullable = true) + Chat chat; + + // Field definition and bounds + @Column(name = "time", nullable = false) + public Date time; + + // Field definition and bounds + @Size(message = "Username length is not valid", min = 5) + @Column(length = 48) + @Size(message = "Username must not be an empty string", min = 1, max = 12 + 32) + @Pattern(message = "Username contains invalid characters", regexp = "^(?:[a-zA-Z\\d][_]?)+$") + @Pattern(message = "Username is not valid", regexp = "^(?:translation_|mv_)?[a-zA-Z]([a-zA-Z_\\d]){1,30}[a-zA-Z\\d]$") + // Keep field history + public String username; + + public static Uni findNewest(Long chatId) { + if (chatId == null) { + throw new NullPointerException("Id must not be null"); + } + return find("from chat_username where chat_id = ?1", Sort.by("time").descending(), chatId).firstResult(); + } + + public static Multi listAll(Long chatId) { + if (chatId == null) { + throw new NullPointerException("Id must not be null"); + } + return find("from chat_username where chat_id = ?1", Sort.by("time").ascending(), chatId).stream(); + } + + public static Uni deleteByChatId(Long chatId) { + return delete("from chat_username where chat_id = ?1", chatId); + } + + @Override public int compareTo(HistoricChatUsername o) { + return o.time.compareTo(this.time); + } + + @Override public String toString() { + return new StringJoiner(", ", HistoricChatUsername.class.getSimpleName() + "[", "]") + .add("id=" + id) + .add("time=" + time) + .add("username='" + username + "'") + .toString(); + } +} diff --git a/service-chats/src/main/resources/import.sql b/service-chats/src/main/resources/import.sql index fe65eb5..4a36823 100644 --- a/service-chats/src/main/resources/import.sql +++ b/service-chats/src/main/resources/import.sql @@ -1,4 +1,14 @@ -INSERT INTO chat(id, name, username, status) VALUES (9007199256673076, 'My Supergroup', 'mysupergroup', 1); -INSERT INTO chat(id, name, username, status) VALUES (777000, 'Telegram', 'telegram', null); -INSERT INTO chat(id, name, username, status) VALUES (4503599627464345, 'School group', null, 1); -INSERT INTO chat(id, name, username, status) VALUES (4503599627382355, 'Old school group', null, 0); +INSERT INTO chat(id, status, name, username) VALUES (9007199256673076, 1, 'My Supergroup', 'mysupergroup'); +INSERT INTO chat_name(chat_id, id, time, name) VALUES (9007199256673076, 12345000, current_timestamp, 'My Supergroup'); +INSERT INTO chat_username(chat_id, id, time, username) VALUES (9007199256673076, 12345001, current_timestamp, 'mysupergroup'); + +INSERT INTO chat(id, status, name, username) VALUES (777000, 1, 'Telegram', 'telegram'); +INSERT INTO chat_name(chat_id, id, time, name) VALUES (777000, 12345002, current_timestamp, 'Telegram'); +INSERT INTO chat_username(chat_id, id, time, username) VALUES (777000, 12345003, current_timestamp, 'telegram'); + + +INSERT INTO chat(id, status, name) VALUES (4503599627464345, 1, 'School group'); +INSERT INTO chat_name(chat_id, id, time, name) VALUES (4503599627464345, 12345004, current_timestamp, 'School group'); + +INSERT INTO chat(id, status, name) VALUES (4503599627382355, 0, 'Old school group'); +INSERT INTO chat_name(chat_id, id, time, name) VALUES (4503599627382355, 12345005, current_timestamp, 'Old school group'); diff --git a/service-chats/src/test/java/io/volvox/chats/ChatServiceTest.java b/service-chats/src/test/java/io/volvox/chats/ChatServiceTest.java new file mode 100644 index 0000000..88a0a59 --- /dev/null +++ b/service-chats/src/test/java/io/volvox/chats/ChatServiceTest.java @@ -0,0 +1,122 @@ +package io.volvox.chats; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.quarkus.hibernate.reactive.panache.Panache; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import javax.inject.Inject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@QuarkusTestResource(ElasticsearchContainerTestResource.class) +public class ChatServiceTest { + + @Inject ChatService chatService; + + @Inject ChatsServiceWarmup chatsServiceWarmup; + + @BeforeEach + public void beforeEach() { + chatsServiceWarmup.warmup(); + } + + @Test + public void testCount() { + var count = chatService.count().await().indefinitely(); + assertEquals(4, count); + } + + @Test + public void testListAll() { + var list = chatService.listAll().map(Chat::getChatId).collect().asList().await().indefinitely(); + assertThat(list) + .containsExactlyInAnyOrder(ChatId.fromLong(9007199256673076L), ChatId.fromLong(777000), + ChatId.fromLong(4503599627464345L), ChatId.fromLong(4503599627382355L) + ); + } + + @Test + public void testSearchByName() { + var chat = chatService.searchByName("Telegram").await().indefinitely(); + assertThat(chat.size()).isEqualTo(1L); + assertThat(chat.get(0).getChatId()).isEqualTo(ChatId.fromLong(777000L)); + } + + @Test + public void testSearchByUsername() { + var chat = chatService.searchByUsername("telegram").await().indefinitely(); + assertThat(chat.size()).isEqualTo(1L); + assertThat(chat.get(0).getChatId()).isEqualTo(ChatId.fromLong(777000)); + } + + @Test + public void testCreateNonexistent() { + var newChat = new Chat(); + newChat.id = 777234L; + newChat.name = "TestChat"; + newChat.username = "uname"; + newChat.status = Status.ALIVE; + chatService.create(newChat).await().indefinitely(); + Panache.getSession().onItem().invoke(() -> { + assertThat(newChat.isPersistent()).isEqualTo(true); + assertThat(newChat.id).isEqualTo(777234L); + assertThat(newChat.name).isEqualTo("TestChat"); + assertThat(newChat.username).isEqualTo("uname"); + assertThat(newChat.status).isEqualTo(Status.ALIVE); + }).await().indefinitely(); + } + + @Test + public void testUpdateNonexistent() { + var chat = new Chat(); + chat.id = 777234L; + chat.name = "TestChat"; + chat.username = "uname"; + chat.status = Status.ALIVE; + var newChat = chatService.update(777234L, chat).await().indefinitely(); + Panache.getSession().onItem().invoke(() -> { + assertThat(newChat).isNotNull(); + assertThat(newChat.isPersistent()).isEqualTo(true); + assertThat(newChat.id).isEqualTo(777234L); + assertThat(newChat.name).isEqualTo("TestChat"); + assertThat(newChat.username).isEqualTo("uname"); + assertThat(newChat.status).isEqualTo(Status.ALIVE); + }).await().indefinitely(); + } + + @Test + public void testUpdateExisting() { + // Create chat + { + var chat = new Chat(); + chat.id = 777234L; + chat.name = "TestChat"; + chat.username = "uname"; + chat.status = Status.ALIVE; + chatService.create(chat).await().indefinitely(); + } + // Test update + { + var chat = new Chat(); + chat.id = 777234L; + chat.username = "mario"; + var newChat = chatService.update(777234L, chat).await().indefinitely(); + Panache.getSession().onItem().invoke(() -> { + assertThat(newChat).isNotNull(); + assertThat(newChat.isPersistent()).isEqualTo(true); + assertThat(newChat.id).isEqualTo(777234L); + assertThat(newChat.name).isEqualTo("TestChat"); + assertThat(newChat.username).isEqualTo("mario"); + assertThat(newChat.status).isEqualTo(Status.ALIVE); + }).await().indefinitely(); + } + } + + @BeforeEach + public void tearDown(){ + Panache.withTransaction(() -> Chat.deleteById(777234L)).await().indefinitely(); + } +} diff --git a/service-chats/src/test/java/io/volvox/chats/ChatsEndpointTest.java b/service-chats/src/test/java/io/volvox/chats/ChatsEndpointTest.java index e607754..4405a78 100644 --- a/service-chats/src/test/java/io/volvox/chats/ChatsEndpointTest.java +++ b/service-chats/src/test/java/io/volvox/chats/ChatsEndpointTest.java @@ -2,15 +2,14 @@ package io.volvox.chats; import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyString; -import static org.hamcrest.Matchers.is; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import io.restassured.response.Response; +import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -18,126 +17,120 @@ import org.junit.jupiter.api.Test; @QuarkusTestResource(ElasticsearchContainerTestResource.class) public class ChatsEndpointTest { - @Test - public void testListAllChats() { - //List all, should have all 3 usernames the database has initially: - Response response = given() - .when() - .get("/chats") - .then() - .statusCode(200) - .contentType("application/json") - .extract().response(); - assertThat(response.jsonPath().getList("name")).containsExactlyInAnyOrder("My Supergroup", "Telegram", "School group", "Old school group"); + @Inject ChatsServiceWarmup chatsServiceWarmup; - // Update Telegram to Telegram Official - given() - .when() - .body("{\"name\" : \"Telegram Official\"}") - .contentType("application/json") - .put("/chats/777000") - .then() - .statusCode(200) - .body( - containsString("\"id\":"), - containsString("\"name\":\"Telegram Official\"")); - - //List all, Telegram Official should've replaced Telegram: - response = given() - .when() - .get("/chats") - .then() - .statusCode(200) - .contentType("application/json") - .extract().response(); - assertThat(response.jsonPath().getList("name")) - .containsExactlyInAnyOrder("My Supergroup", "Telegram Official", "School group", "Old school group"); - - //Delete Telegram: - given() - .when() - .delete("/chats/777000") - .then() - .statusCode(204); - - response = given() - .when() - .get("/chats") - .then() - .statusCode(200) - .contentType("application/json") - .extract().response(); - assertThat(response.jsonPath().getList("name")) - .containsExactlyInAnyOrder("My Supergroup", "School group", "Old school group"); - - //Create Telegram2: - given() - .when() - .body("{\"id\": \"777001-u\", \"name\" : \"Telegram2\"}") - .contentType("application/json") - .post("/chats") - .then() - .statusCode(201) - .body(emptyString()); - - //List all, Pineapple should be still missing now: - response = given() - .when() - .get("/chats") - .then() - .statusCode(200) - .extract().response(); - assertThat(response.jsonPath().getList("name")) - .containsExactlyInAnyOrder("My Supergroup", "School group", "Old school group", "Telegram2"); - } - - @Test - public void testEntityNotFoundForDelete() { - given() - .when() - .delete("/chats/777123") - .then() - .statusCode(404) - .body(emptyString()); - } - - @Test - public void testEntityNotFoundForUpdate() { - given() - .when() - .body("{\"id\": \"777234-u\", \"name\" : \"Juan\"}") - .contentType("application/json") - .put("/chats/777234") - .then() - .statusCode(200); - } - - @Test - public void testEntityWithoutIdForUpdate() { - given() - .when() - .body("{\"name\" : \"Juan\"}") - .contentType("application/json") - .put("/chats/777234") - .then() - .statusCode(500); - } + @BeforeEach + public void beforeEach() { + chatsServiceWarmup.warmup(); + } @Test - public void testHealth() { - given() + public void testListAllChats() { + //List all, should have all 3 usernames the database has initially: + Response response = given() .when() - .get("/q/health/ready") + .get("/chats") .then() .statusCode(200) .contentType("application/json") - .body("status", is("UP"), - "checks.status", containsInAnyOrder("UP"), - "checks.name", containsInAnyOrder("Elasticsearch cluster health check")); + .extract().response(); + assertThat(response.jsonPath().getList("name")).containsExactlyInAnyOrder("My Supergroup", "Telegram", "School group", "Old school group"); + + // Update Telegram to Telegram Official + given() + .when() + .body("{\"name\" : \"Telegram Official\"}") + .contentType("application/json") + .put("/chats/777000") + .then() + .statusCode(200) + .body( + containsString("\"id\":"), + containsString("\"name\":\"Telegram Official\"")); + + //List all, Telegram Official should've replaced Telegram: + response = given() + .when() + .get("/chats") + .then() + .statusCode(200) + .contentType("application/json") + .extract().response(); + assertThat(response.jsonPath().getList("name")) + .containsExactlyInAnyOrder("My Supergroup", "Telegram Official", "School group", "Old school group"); + + //Delete Telegram: + given() + .when() + .delete("/chats/777000") + .then() + .statusCode(204); + + response = given() + .when() + .get("/chats") + .then() + .statusCode(200) + .contentType("application/json") + .extract().response(); + assertThat(response.jsonPath().getList("name")) + .containsExactlyInAnyOrder("My Supergroup", "School group", "Old school group"); + + //Create Telegram2: + given() + .when() + .body("{\"id\": \"777001-u\", \"name\" : \"Telegram2\"}") + .contentType("application/json") + .post("/chats") + .then() + .statusCode(201) + .body(emptyString()); + + //List all, Pineapple should be still missing now: + response = given() + .when() + .get("/chats") + .then() + .statusCode(200) + .extract().response(); + assertThat(response.jsonPath().getList("name")) + .containsExactlyInAnyOrder("My Supergroup", "School group", "Old school group", "Telegram2"); } - @BeforeEach - public void tearDown(){ - Panache.withTransaction(() -> Chat.deleteById(777234L)).await().indefinitely(); - } + @Test + public void testEntityNotFoundForDelete() { + given() + .when() + .delete("/chats/777123") + .then() + .statusCode(404) + .body(emptyString()); + } + + @Test + public void testEntityNotFoundForUpdate() { + given() + .when() + .body("{\"id\": \"777234-u\", \"name\" : \"Juan\"}") + .contentType("application/json") + .put("/chats/777234") + .then() + .statusCode(200); + } + + @Test + public void testEntityWithoutIdForUpdate() { + given() + .when() + .body("{\"name\" : \"Juan\"}") + .contentType("application/json") + .put("/chats/777234") + .then() + .statusCode(500); + } + + @BeforeEach + public void tearDown(){ + Panache.withTransaction(() -> Chat.deleteById(777234L)).await().indefinitely(); + } } diff --git a/service-chats/src/test/java/io/volvox/chats/ChatsServiceWarmup.java b/service-chats/src/test/java/io/volvox/chats/ChatsServiceWarmup.java new file mode 100644 index 0000000..db52bb7 --- /dev/null +++ b/service-chats/src/test/java/io/volvox/chats/ChatsServiceWarmup.java @@ -0,0 +1,59 @@ +package io.volvox.chats; + +import io.quarkus.logging.Log; +import io.reactiverse.elasticsearch.client.mutiny.RestHighLevelClient; +import io.smallrye.mutiny.Uni; +import io.vertx.core.json.JsonObject; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.action.admin.indices.flush.FlushRequest; +import org.elasticsearch.action.admin.indices.refresh.RefreshRequest; +import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.action.update.UpdateResponse; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.indices.CreateIndexRequest; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.rest.RestStatus; + +@ApplicationScoped +public class ChatsServiceWarmup { + @Inject + ChatService chatService; + @Inject + RestHighLevelClient restHighLevelClient; + + + public void warmup() { + createIndices(); + chatService.listAll().onItem().transformToUni(this::updateIndex).merge().select().last().toUni().await() + .indefinitely(); + + restHighLevelClient.indices().flushAsyncAndAwait(new FlushRequest("chats"), RequestOptions.DEFAULT); + restHighLevelClient.indices().refreshAsyncAndAwait(new RefreshRequest("chats"), RequestOptions.DEFAULT); + } + + private void createIndices() { + var req = new CreateIndexRequest("chats"); + try { + restHighLevelClient.indices().createAsyncAndAwait(req, RequestOptions.DEFAULT); + } catch (ElasticsearchStatusException ex) { + if (ex.status() != RestStatus.BAD_REQUEST) { + throw ex; + } + } + } + + private Uni updateIndex(Chat chat) { + var request = new UpdateRequest("chats", ChatId.toString(chat.id)).docAsUpsert(true); + request.doc(JsonObject.mapFrom(chat).toString(), XContentType.JSON); + + Log.infof("Index chat %s", chat); + + return restHighLevelClient.updateAsync(request, RequestOptions.DEFAULT).onItem().invoke(response -> { + if (response.status() != RestStatus.CREATED && response.status() != RestStatus.OK) { + throw new UnsupportedOperationException("Unexpected status: " + response.status().toString()); + } + }); + } +} diff --git a/service-chats/src/test/java/io/volvox/chats/ElasticsearchContainerTestResource.java b/service-chats/src/test/java/io/volvox/chats/ElasticsearchContainerTestResource.java index 834fb73..065c8d7 100644 --- a/service-chats/src/test/java/io/volvox/chats/ElasticsearchContainerTestResource.java +++ b/service-chats/src/test/java/io/volvox/chats/ElasticsearchContainerTestResource.java @@ -1,12 +1,10 @@ package io.volvox.chats; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; import java.util.Map; - import org.testcontainers.elasticsearch.ElasticsearchContainer; import org.testcontainers.utility.DockerImageName; -import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; - public class ElasticsearchContainerTestResource implements QuarkusTestResourceLifecycleManager { static ElasticsearchContainer elasticsearchContainer = new ElasticsearchContainer( @@ -17,8 +15,7 @@ public class ElasticsearchContainerTestResource implements QuarkusTestResourceLi public Map start() { elasticsearchContainer.withEnv("action.auto_create_index", "true"); elasticsearchContainer.start(); - return Map.of( - "quarkus.elasticsearch.hosts", elasticsearchContainer.getHttpHostAddress()); + return Map.of("quarkus.elasticsearch.hosts", elasticsearchContainer.getHttpHostAddress()); } @Override diff --git a/service-chats/src/test/java/io/volvox/chats/IndexWarmupTest.java b/service-chats/src/test/java/io/volvox/chats/IndexWarmupTest.java new file mode 100644 index 0000000..c6b2ba7 --- /dev/null +++ b/service-chats/src/test/java/io/volvox/chats/IndexWarmupTest.java @@ -0,0 +1,29 @@ +package io.volvox.chats; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.reactiverse.elasticsearch.client.mutiny.RestHighLevelClient; +import javax.inject.Inject; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.core.CountRequest; +import org.elasticsearch.rest.RestStatus; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@QuarkusTestResource(ElasticsearchContainerTestResource.class) +public class IndexWarmupTest { + @Inject + RestHighLevelClient restHighLevelClient; + + @Inject ChatsServiceWarmup chatsServiceWarmup; + + @Test + public void test() { + chatsServiceWarmup.warmup(); + var count = restHighLevelClient.countAsyncAndAwait(new CountRequest("chats"), RequestOptions.DEFAULT); + assertEquals(RestStatus.OK, count.status()); + assertEquals(4, count.getCount()); + } +}