Implement chats service tests

This commit is contained in:
Andrea Cavalli 2021-11-26 18:23:44 +01:00
parent 9c76626c70
commit fb2427518d
13 changed files with 394 additions and 51 deletions

View File

@ -16,8 +16,6 @@
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>2.4.1.Final</quarkus.platform.version>
<surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
<volvox.tdlight.version>2.7.9.2</volvox.tdlight.version>
<volvox.tdlight.natives.version>4.0.183</volvox.tdlight.natives.version>
</properties>
<repositories>
<repository>
@ -87,18 +85,10 @@
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-java</artifactId>
</dependency>
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-natives-linux-amd64</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -1,23 +1,58 @@
package io.volvox.chats;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.quarkus.hibernate.reactive.panache.PanacheEntityBase;
import java.util.StringJoiner;
import javax.persistence.Cacheable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.validation.constraints.Max;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Positive;
import org.hibernate.validator.constraints.Length;
@Entity
@Cacheable
public class Chat extends PanacheEntityBase {
@Id
public String id;
@Id
@Positive(message = "id is not positive")
@Max(message = "id is too big", value = ChatId.MASK)
@Column(nullable = false, unique = true)
@JsonSerialize(using = ChatIdJsonSerializer.class)
@JsonDeserialize(using = ChatIdJsonDeserializer.class)
public Long id;
@Column(length = 128)
public String name;
@Length(message = "Username length is not valid", min = 5)
@Column(length = 48)
@Length(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]$")
public String username;
@Enumerated
public Status status;
@JsonIgnore
public ChatId getChatId() {
return new ChatId(id);
return ChatId.fromLong(id);
}
@JsonIgnore
public void setChatId(ChatId id) {
this.id = id.toString();
this.id = id.toLong();
}
@Override public String toString() {
return new StringJoiner(", ", Chat.class.getSimpleName() + "[", "]")
.add("id=" + id)
.add("name='" + name + "'")
.add("username='" + username + "'")
.add("status=" + status)
.toString();
}
}

View File

@ -1,35 +1,116 @@
package io.volvox.chats;
public record ChatId(Type type, long id) {
ChatId(String id) {
this(getType(id), getIdLong(id));
public record ChatId(Type type, long subId) {
public static final int SUB_ID_MASK_BYTES = 52;
public static final int TYPE_MASK_BYTES = 2;
public static final long SUB_ID_MASK = 0b001111111111111111111111111111111111111111111111111111L;
public static final long TYPE_MASK = 0b11L << SUB_ID_MASK_BYTES;
public static final long MASK = SUB_ID_MASK | TYPE_MASK;
public static final int TYPE_PRIVATE_INT = 0b00;
public static final int TYPE_BASIC_INT = 0b01;
public static final int TYPE_SUPER_INT = 0b10;
public static final int TYPE_SECRET_INT = 0b11;
public static final long TYPE_PRIVATE_LONG = 0;
public static final long TYPE_BASIC_LONG = 0b01L << SUB_ID_MASK_BYTES & TYPE_MASK;
public static final long TYPE_SUPER_LONG = 0b10L << SUB_ID_MASK_BYTES & TYPE_MASK;
public static final long TYPE_SECRET_LONG = 0b11L << SUB_ID_MASK_BYTES & TYPE_MASK;
public ChatId {
if ((subId & SUB_ID_MASK) != subId) {
throw new IllegalArgumentException("subId is too big");
}
}
private static Type getType(String id) {
return switch (id.charAt(0)) {
case 's' -> Type.SUPER;
case 'b' -> Type.BASIC;
case 'u' -> Type.PRIVATE;
default -> throw new IllegalArgumentException();
public static ChatId fromLong(long id) {
return new ChatId(getType(id), getIdLong(id));
}
private static Type getType(long id) {
return switch ((int) ((id & TYPE_MASK) >> SUB_ID_MASK_BYTES)) {
case TYPE_SUPER_INT -> Type.SUPER;
case TYPE_BASIC_INT -> Type.BASIC;
case TYPE_PRIVATE_INT -> Type.PRIVATE;
case TYPE_SECRET_INT -> Type.SECRET;
default -> throw new IllegalArgumentException("Invalid id type: " + id);
};
}
private static long getIdLong(String id) {
return Long.parseUnsignedLong(id.substring(1));
private static long getIdLong(long id) {
return id & SUB_ID_MASK;
}
public long toLong() {
return switch (type) {
case SUPER -> TYPE_SUPER_LONG;
case BASIC -> TYPE_BASIC_LONG;
case PRIVATE -> TYPE_PRIVATE_LONG;
case SECRET -> TYPE_SECRET_LONG;
} | (subId & SUB_ID_MASK);
}
public enum Type {
PRIVATE,
BASIC,
SUPER,
PRIVATE
SECRET
}
@Override
public String toString() {
return switch (type) {
return toString(this);
}
public static String toString(ChatId chatId) {
return Long.toUnsignedString(chatId.subId) + "-" + switch (chatId.type) {
case SUPER -> 's';
case BASIC -> 'b';
case PRIVATE -> 'u';
} + Long.toUnsignedString(id);
case SECRET -> 'd';
};
}
public static String toString(long chatId) {
return Long.toUnsignedString(getIdLong(chatId)) + "-" + switch (getType(chatId)) {
case SUPER -> 's';
case BASIC -> 'b';
case PRIVATE -> 'u';
case SECRET -> 'd';
};
}
public static ChatId fromString(String chatId) {
var parts = chatId.split("-", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("Malformed chat id");
}
if (parts[1].length() != 1) {
throw new IllegalArgumentException("Chat type is too long");
}
return new ChatId(switch(parts[1].charAt(0)) {
case 's' -> Type.SUPER;
case 'b' -> Type.BASIC;
case 'u' -> Type.PRIVATE;
case 'd' -> Type.SECRET;
default -> throw new IllegalStateException("Unexpected value: " + parts[1].charAt(0));
}, Long.parseUnsignedLong(parts[0]) & SUB_ID_MASK);
}
public static Long stringToLong(String chatId) {
var parts = chatId.split("-", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("Malformed chat id");
}
if (parts[1].length() != 1) {
throw new IllegalArgumentException("Chat type is too long");
}
return switch(parts[1].charAt(0)) {
case 's' -> TYPE_SUPER_LONG;
case 'b' -> TYPE_BASIC_LONG;
case 'u' -> TYPE_PRIVATE_LONG;
case 'd' -> TYPE_SECRET_LONG;
default -> throw new IllegalStateException("Unexpected value: " + parts[1].charAt(0));
} | (Long.parseUnsignedLong(parts[0]) & SUB_ID_MASK);
}
}

View File

@ -0,0 +1,15 @@
package io.volvox.chats;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
public class ChatIdJsonDeserializer extends JsonDeserializer<Long> {
@Override
public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
var id = p.readValueAs(String.class);
return ChatId.stringToLong(id);
}
}

View File

@ -0,0 +1,14 @@
package io.volvox.chats;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
public class ChatIdJsonSerializer extends JsonSerializer<Long> {
@Override
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(ChatId.toString(value));
}
}

View File

@ -14,7 +14,7 @@ import org.hibernate.annotations.NamedQuery;
@NamedQuery(name = "Chat.updateStatusById", query = "update Chat p set p.status = :status where p.id = :id"),
@NamedQuery(name = "Chat.deleteById", query = "delete from Chat p where p.id = ?1")
})
public class ChatRepository implements PanacheRepositoryBase<Chat, String> {
public class ChatRepository implements PanacheRepositoryBase<Chat, Long> {
public Uni<Chat> findByUsername(String username) {
return find("#Chat.getByUsername", username).firstResult();

View File

@ -1,11 +1,10 @@
package io.volvox.chats;
import io.quarkus.hibernate.reactive.panache.PanacheEntityBase;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import java.net.URI;
import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
@ -33,24 +32,21 @@ public class ChatResource {
@GET
@Path("/{id}")
public Uni<Chat> get(@PathParam("id") String id) {
public Uni<Chat> get(@PathParam("id") Long id) {
return chatRepository.findById(id);
}
@POST
@Transactional
public Uni<Response> create(Chat chat) {
return chatRepository.persist(chat)
// Return success
.replaceWith(() -> Response.created(URI.create("/api/" + chat.id)).build());
return Panache.withTransaction(() -> chatRepository.persist(chat))
.onItem().transform(inserted -> Response.created(URI.create("/api/" + chat.id)).build());
}
@PUT
@Path("/{id}")
@Transactional
public Uni<Chat> update(@PathParam("id") String id, Chat chat) {
public Uni<Chat> update(@PathParam("id") Long id, Chat chat) {
// Find chat by id
return chatRepository.<Chat>findById(id)
return Panache.withTransaction(() -> chatRepository.findById(id)
.flatMap(entity -> {
if (entity == null) {
// Persist the chat if not found
@ -61,16 +57,15 @@ public class ChatResource {
// Return the updated item
return Uni.createFrom().item(entity);
}
});
}));
}
@DELETE
@Path("/{id}")
@Transactional
public Uni<Void> delete(@PathParam("id") String id) {
return chatRepository.findById(id)
public Uni<Void> delete(@PathParam("id") Long id) {
return Panache.withTransaction(() -> chatRepository.findById(id)
.onItem().ifNull().failWith(NotFoundException::new)
.flatMap(PanacheEntityBase::delete);
.flatMap(chatRepository::delete));
}
@GET

View File

@ -21,7 +21,7 @@ public class ChatsService {
}
@ConsumeEvent(value = "chats.get")
public void get(Message<String> msg) {
public void get(Message<Long> msg) {
chatResource.get(msg.body()).subscribe().with(msg::reply);
}

View File

@ -1,7 +1,6 @@
package io.volvox.chats;
public enum Status {
ALIVE,
DELETED,
UNKNOWN
DEAD,
ALIVE
}

View File

@ -0,0 +1,10 @@
%prod.quarkus.datasource.db-kind=postgresql
%prod.quarkus.datasource.username=quarkus_test
%prod.quarkus.datasource.password=quarkus_test
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.sql-load-script=import.sql
# Reactive config
%prod.quarkus.datasource.reactive.url=vertx-reactive:postgresql://localhost/quarkus_test

View File

@ -0,0 +1,4 @@
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);

View File

@ -0,0 +1,70 @@
package io.volvox.chats;
import static org.junit.jupiter.api.Assertions.assertEquals;
import io.volvox.chats.ChatId.Type;
import org.junit.jupiter.api.Test;
public class ChatIdTest {
@Test
public void newChatId() {
assertEquals(Type.BASIC, new ChatId(Type.BASIC, 10).type());
assertEquals(Type.PRIVATE, new ChatId(Type.PRIVATE, 10).type());
assertEquals(Type.SECRET, new ChatId(Type.SECRET, 10).type());
assertEquals(Type.SUPER, new ChatId(Type.SUPER, 10).type());
assertEquals(1, new ChatId(Type.BASIC, 1).subId());
assertEquals(2, new ChatId(Type.PRIVATE, 2).subId());
assertEquals(3, new ChatId(Type.SECRET, 3).subId());
assertEquals(4, new ChatId(Type.SUPER, 4).subId());
}
@Test
public void toLong() {
assertEquals(777000, new ChatId(Type.PRIVATE, 777000).toLong());
assertEquals(0b01L << 52 | 777000, new ChatId(Type.BASIC, 777000).toLong());
assertEquals(0b10L << 52 | 777000, new ChatId(Type.SUPER, 777000).toLong());
assertEquals(0b11L << 52 | 777000, new ChatId(Type.SECRET, 777000).toLong());
}
@Test
public void fromLong() {
assertEquals(new ChatId(Type.PRIVATE, 777000), ChatId.fromLong(777000));
assertEquals(new ChatId(Type.BASIC, 777000), ChatId.fromLong(0b01L << 52 | 777000));
assertEquals(new ChatId(Type.SUPER, 777000), ChatId.fromLong(0b10L << 52 | 777000));
assertEquals(new ChatId(Type.SECRET, 777000), ChatId.fromLong(0b11L << 52 | 777000));
}
@Test
public void fromString() {
assertEquals(new ChatId(Type.PRIVATE, 777000), ChatId.fromString("777000-u"));
assertEquals(new ChatId(Type.BASIC, 777000), ChatId.fromString("777000-b"));
assertEquals(new ChatId(Type.SUPER, 777000), ChatId.fromString("777000-s"));
assertEquals(new ChatId(Type.SECRET, 777000), ChatId.fromString("777000-d"));
}
@Test
public void testToString() {
assertEquals("777000-u", new ChatId(Type.PRIVATE, 777000).toString());
assertEquals("777000-b", new ChatId(Type.BASIC, 777000).toString());
assertEquals("777000-s", new ChatId(Type.SUPER, 777000).toString());
assertEquals("777000-d", new ChatId(Type.SECRET, 777000).toString());
}
@Test
public void longToString() {
assertEquals("777000-u", ChatId.toString(777000));
assertEquals("777000-b", ChatId.toString(0b01L << 52 | 777000));
assertEquals("777000-s", ChatId.toString(0b10L << 52 | 777000));
assertEquals("777000-d", ChatId.toString(0b11L << 52 | 777000));
}
@Test
public void stringToLong() {
assertEquals(777000, ChatId.stringToLong("777000-u"));
assertEquals(0b01L << 52 | 777000, ChatId.stringToLong("777000-b"));
assertEquals(0b10L << 52 | 777000, ChatId.stringToLong("777000-s"));
assertEquals(0b11L << 52 | 777000, ChatId.stringToLong("777000-d"));
}
}

View File

@ -0,0 +1,130 @@
package io.volvox.chats;
import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.emptyString;
import io.quarkus.hibernate.reactive.panache.Panache;
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;
@QuarkusTest
public class ChatsEndpointTest {
@Inject
ChatRepository chatRepository;
@Test
public void testListAllChats() {
//List all, should have all 3 usernames the database has initially:
Response response = given()
.when()
.get("/api/chats")
.then()
.statusCode(200)
.contentType("application/json")
.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("/api/chats/777000")
.then()
.statusCode(200)
.body(
containsString("\"id\":"),
containsString("\"name\":\"Telegram Official\""));
//List all, Telegram Official should've replaced Telegram:
response = given()
.when()
.get("/api/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("/api/chats/777000")
.then()
.statusCode(204);
response = given()
.when()
.get("/api/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("/api/chats")
.then()
.statusCode(201)
.body(emptyString());
//List all, Pineapple should be still missing now:
response = given()
.when()
.get("/api/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("/api/chats/777123")
.then()
.statusCode(404)
.body(emptyString());
}
@Test
public void testEntityNotFoundForUpdate() {
given()
.when()
.body("{\"id\": \"777234-u\", \"name\" : \"Juan\"}")
.contentType("application/json")
.put("/api/chats/777234")
.then()
.statusCode(200);
}
@Test
public void testEntityWithoutIdForUpdate() {
given()
.when()
.body("{\"name\" : \"Juan\"}")
.contentType("application/json")
.put("/api/chats/777234")
.then()
.statusCode(500);
}
@BeforeEach
public void tearDown(){
Panache.withTransaction(() -> chatRepository.deleteById(777234L)).await().indefinitely();
}
}