From be9a6139c29da38c27f55f1f85a427991fd72b37 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 16 Jan 2025 18:56:01 +0530 Subject: [PATCH 01/10] [ECO-5193][TM*] Updated innner Message class 1. Added missing fields for refSerial, refType and Operation 2. Added serialization and deserialization for above fields using msgpack 3. Added serialization and deserialization for above fields using gson --- .../main/java/io/ably/lib/types/Message.java | 146 +++++++++++++++++- 1 file changed, 145 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/io/ably/lib/types/Message.java b/lib/src/main/java/io/ably/lib/types/Message.java index 99eed55f5..463a361bb 100644 --- a/lib/src/main/java/io/ably/lib/types/Message.java +++ b/lib/src/main/java/io/ably/lib/types/Message.java @@ -3,6 +3,9 @@ import java.io.IOException; import java.lang.reflect.Type; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + import com.google.gson.JsonArray; import com.google.gson.JsonDeserializer; import com.google.gson.JsonDeserializationContext; @@ -74,6 +77,94 @@ public class Message extends BaseMessage { */ public Long createdAt; + /** + * (TM2l) ref string – an opaque string that uniquely identifies some referenced message. + */ + public String refSerial; + + /** + * (TM2m) refType string – an opaque string that identifies the type of this reference. + */ + public String refType; + + /** + * (TM2n) operation object – data object that may contain the `optional` attributes. + */ + public Operation operation; + + public static class Operation { + public String clientId; + public String description; + public Map metadata; + + void write(MessagePacker packer) throws IOException { + packer.packMapHeader(3); + if(clientId != null) { + packer.packString("clientId"); + packer.packString(clientId); + } + if(description != null) { + packer.packString("description"); + packer.packString(description); + } + if(metadata != null) { + packer.packString("metadata"); + packer.packMapHeader(metadata.size()); + for(Map.Entry entry : metadata.entrySet()) { + packer.packString(entry.getKey()); + packer.packString(entry.getValue()); + } + } + } + + protected static Operation read(final MessageUnpacker unpacker) throws IOException { + Operation operation = new Operation(); + int fieldCount = unpacker.unpackMapHeader(); + for (int i = 0; i < fieldCount; i++) { + String fieldName = unpacker.unpackString().intern(); + switch (fieldName) { + case "clientId": + operation.clientId = unpacker.unpackString(); + break; + case "description": + operation.description = unpacker.unpackString(); + break; + case "metadata": + int mapSize = unpacker.unpackMapHeader(); + operation.metadata = new HashMap<>(mapSize); + for (int j = 0; j < mapSize; j++) { + String key = unpacker.unpackString(); + String value = unpacker.unpackString(); + operation.metadata.put(key, value); + } + break; + default: + unpacker.skipValue(); + break; + } + } + return operation; + } + + protected static Operation read(final JsonObject jsonObject) throws MessageDecodeException { + Operation operation = new Operation(); + if (jsonObject.has("clientId")) { + operation.clientId = jsonObject.get("clientId").getAsString(); + } + if (jsonObject.has("description")) { + operation.description = jsonObject.get("description").getAsString(); + } + if (jsonObject.has("metadata")) { + JsonObject metadataObject = jsonObject.getAsJsonObject("metadata"); + operation.metadata = new HashMap<>(); + for (Map.Entry entry : metadataObject.entrySet()) { + operation.metadata.put(entry.getKey(), entry.getValue().getAsString()); + } + } + return operation; + } + } + private static final String NAME = "name"; private static final String EXTRAS = "extras"; private static final String CONNECTION_KEY = "connectionKey"; @@ -81,6 +172,9 @@ public class Message extends BaseMessage { private static final String VERSION = "version"; private static final String ACTION = "action"; private static final String CREATED_AT = "createdAt"; + private static final String REF_SERIAL = "refSerial"; + private static final String REF_TYPE = "refType"; + private static final String OPERATION = "operation"; /** * Default constructor @@ -160,10 +254,15 @@ void writeMsgpack(MessagePacker packer) throws IOException { int fieldCount = super.countFields(); if(name != null) ++fieldCount; if(extras != null) ++fieldCount; + if(connectionKey != null) ++fieldCount; if(serial != null) ++fieldCount; if(version != null) ++fieldCount; if(action != null) ++fieldCount; if(createdAt != null) ++fieldCount; + if(refSerial != null) ++fieldCount; + if(refType != null) ++fieldCount; + if(operation != null) ++fieldCount; + packer.packMapHeader(fieldCount); super.writeFields(packer); if(name != null) { @@ -174,6 +273,10 @@ void writeMsgpack(MessagePacker packer) throws IOException { packer.packString(EXTRAS); extras.write(packer); } + if(connectionKey != null) { + packer.packString(CONNECTION_KEY); + packer.packString(connectionKey); + } if(serial != null) { packer.packString(SERIAL); packer.packString(serial); @@ -190,6 +293,18 @@ void writeMsgpack(MessagePacker packer) throws IOException { packer.packString(CREATED_AT); packer.packLong(createdAt); } + if(refSerial != null) { + packer.packString(REF_SERIAL); + packer.packString(refSerial); + } + if(refType != null) { + packer.packString(REF_TYPE); + packer.packString(refType); + } + if(operation != null) { + packer.packString(OPERATION); + operation.write(packer); + } } Message readMsgpack(MessageUnpacker unpacker) throws IOException { @@ -209,6 +324,8 @@ Message readMsgpack(MessageUnpacker unpacker) throws IOException { name = unpacker.unpackString(); } else if (fieldName.equals(EXTRAS)) { extras = MessageExtras.read(unpacker); + } else if (fieldName.equals(CONNECTION_KEY)) { + connectionKey = unpacker.unpackString(); } else if (fieldName.equals(SERIAL)) { serial = unpacker.unpackString(); } else if (fieldName.equals(VERSION)) { @@ -217,7 +334,14 @@ Message readMsgpack(MessageUnpacker unpacker) throws IOException { action = MessageAction.tryFindByOrdinal(unpacker.unpackInt()); } else if (fieldName.equals(CREATED_AT)) { createdAt = unpacker.unpackLong(); - } else { + } else if (fieldName.equals(REF_SERIAL)) { + refSerial = unpacker.unpackString(); + } else if (fieldName.equals(REF_TYPE)) { + refType = unpacker.unpackString(); + } else if (fieldName.equals(OPERATION)) { + operation = Operation.read(unpacker); + } + else { Log.v(TAG, "Unexpected field: " + fieldName); unpacker.skipValue(); } @@ -373,12 +497,23 @@ protected void read(final JsonObject map) throws MessageDecodeException { } extras = MessageExtras.read((JsonObject) extrasElement); } + connectionKey = readString(map, CONNECTION_KEY); serial = readString(map, SERIAL); version = readString(map, VERSION); Integer actionOrdinal = readInt(map, ACTION); action = actionOrdinal == null ? null : MessageAction.tryFindByOrdinal(actionOrdinal); createdAt = readLong(map, CREATED_AT); + refSerial = readString(map, REF_SERIAL); + refType = readString(map, REF_TYPE); + + final JsonElement operationElement = map.get(OPERATION); + if (null != operationElement) { + if (!(operationElement instanceof JsonObject)) { + throw MessageDecodeException.fromDescription("Message operation is of type \"" + operationElement.getClass() + "\" when expected a JSON object."); + } + operation = Operation.read((JsonObject) operationElement); + } } public static class Serializer implements JsonSerializer, JsonDeserializer { @@ -406,6 +541,15 @@ public JsonElement serialize(Message message, Type typeOfMessage, JsonSerializat if (message.createdAt != null) { json.addProperty(CREATED_AT, message.createdAt); } + if (message.refSerial != null) { + json.addProperty(REF_SERIAL, message.refSerial); + } + if (message.refType != null) { + json.addProperty(REF_TYPE, message.refType); + } + if (message.operation != null) { + json.add(OPERATION, Serialisation.gson.toJsonTree(message.operation)); + } return json; } From e8fe70b4c6e969664150a98c79057aa6fbcb4c08 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 17 Jan 2025 14:52:53 +0530 Subject: [PATCH 02/10] [ECO-5193][TM*] Added unit tests to MessageTest 1. Added serializer test for fields refSerial, refType and Operation 2. Added deserializer test for fields refSerial, refType and Operation 3. Added msgpack unit test for Message class --- .../java/io/ably/lib/types/MessageTest.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/lib/src/test/java/io/ably/lib/types/MessageTest.java b/lib/src/test/java/io/ably/lib/types/MessageTest.java index 1873aa7af..3a7997725 100644 --- a/lib/src/test/java/io/ably/lib/types/MessageTest.java +++ b/lib/src/test/java/io/ably/lib/types/MessageTest.java @@ -6,7 +6,13 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.ably.lib.types.Message.Serializer; +import io.ably.lib.util.Serialisation; import org.junit.Test; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.MessageUnpacker; + +import java.io.ByteArrayOutputStream; +import java.util.HashMap; public class MessageTest { @@ -90,6 +96,75 @@ public void deserialize_message_with_serial() throws Exception { assertEquals("01826232498871-001@abcdefghij:001", message.serial); } + @Test + public void serialize_message_with_operation() { + // Given + Message message = new Message("test-name", "test-data"); + message.clientId = "test-client-id"; + message.connectionKey = "test-key"; + message.refSerial = "test-ref-serial"; + message.refType = "test-ref-type"; + Message.Operation operation = new Message.Operation(); + operation.clientId = "operation-client-id"; + operation.description = "operation-description"; + operation.metadata = new HashMap<>(); + operation.metadata.put("key1", "value1"); + operation.metadata.put("key2", "value2"); + message.operation = operation; + + // When + JsonElement serializedElement = serializer.serialize(message, null, null); + + // Then + JsonObject serializedObject = serializedElement.getAsJsonObject(); + assertEquals("test-client-id", serializedObject.get("clientId").getAsString()); + assertEquals("test-key", serializedObject.get("connectionKey").getAsString()); + assertEquals("test-data", serializedObject.get("data").getAsString()); + assertEquals("test-name", serializedObject.get("name").getAsString()); + assertEquals("test-ref-serial", serializedObject.get("refSerial").getAsString()); + assertEquals("test-ref-type", serializedObject.get("refType").getAsString()); + JsonObject operationObject = serializedObject.getAsJsonObject("operation"); + assertEquals("operation-client-id", operationObject.get("clientId").getAsString()); + assertEquals("operation-description", operationObject.get("description").getAsString()); + JsonObject metadataObject = operationObject.getAsJsonObject("metadata"); + assertEquals("value1", metadataObject.get("key1").getAsString()); + assertEquals("value2", metadataObject.get("key2").getAsString()); + } + + @Test + public void deserialize_message_with_operation() throws Exception { + // Given + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("clientId", "test-client-id"); + jsonObject.addProperty("data", "test-data"); + jsonObject.addProperty("name", "test-name"); + jsonObject.addProperty("refSerial", "test-ref-serial"); + jsonObject.addProperty("refType", "test-ref-type"); + jsonObject.addProperty("connectionKey", "test-key"); + JsonObject operationObject = new JsonObject(); + operationObject.addProperty("clientId", "operation-client-id"); + operationObject.addProperty("description", "operation-description"); + JsonObject metadataObject = new JsonObject(); + metadataObject.addProperty("key1", "value1"); + metadataObject.addProperty("key2", "value2"); + operationObject.add("metadata", metadataObject); + jsonObject.add("operation", operationObject); + + // When + Message message = Message.fromEncoded(jsonObject, new ChannelOptions()); + + // Then + assertEquals("test-client-id", message.clientId); + assertEquals("test-data", message.data); + assertEquals("test-name", message.name); + assertEquals("test-ref-serial", message.refSerial); + assertEquals("test-ref-type", message.refType); + assertEquals("test-key", message.connectionKey); + assertEquals("operation-client-id", message.operation.clientId); + assertEquals("operation-description", message.operation.description); + assertEquals("value1", message.operation.metadata.get("key1")); + assertEquals("value2", message.operation.metadata.get("key2")); + } @Test public void deserialize_message_with_unknown_action() throws Exception { @@ -111,4 +186,48 @@ public void deserialize_message_with_unknown_action() throws Exception { assertNull(message.action); assertEquals("01826232498871-001@abcdefghij:001", message.serial); } + + @Test + public void serialize_and_deserialize_with_msgpack() throws Exception { + // Given + Message message = new Message("test-name", "test-data"); + message.clientId = "test-client-id"; + message.connectionKey = "test-key"; + message.refSerial = "test-ref-serial"; + message.refType = "test-ref-type"; + message.action = MessageAction.MESSAGE_CREATE; + message.serial = "01826232498871-001@abcdefghij:001"; + Message.Operation operation = new Message.Operation(); + operation.clientId = "operation-client-id"; + operation.description = "operation-description"; + operation.metadata = new HashMap<>(); + operation.metadata.put("key1", "value1"); + operation.metadata.put("key2", "value2"); + message.operation = operation; + + // When Encode to MessagePack + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); + message.writeMsgpack(packer); + packer.close(); + + // Decode from MessagePack + MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(out.toByteArray()); + Message unpacked = Message.fromMsgpack(unpacker); + unpacker.close(); + + // Then + assertEquals("test-client-id", unpacked.clientId); + assertEquals("test-key", unpacked.connectionKey); + assertEquals("test-data", unpacked.data); + assertEquals("test-name", unpacked.name); + assertEquals("test-ref-serial", unpacked.refSerial); + assertEquals("test-ref-type", unpacked.refType); + assertEquals(MessageAction.MESSAGE_CREATE, unpacked.action); + assertEquals("01826232498871-001@abcdefghij:001", unpacked.serial); + assertEquals("operation-client-id", unpacked.operation.clientId); + assertEquals("operation-description", unpacked.operation.description); + assertEquals("value1", unpacked.operation.metadata.get("key1")); + assertEquals("value2", unpacked.operation.metadata.get("key2")); + } } From 52e04154593f9ca8c27ebcaaa19ea439658aa64b Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 17 Jan 2025 18:08:57 +0530 Subject: [PATCH 03/10] [ECO-5193][TM*] Added test helper for Chat message edit, update and delete 1. Added ChatRoom class that provides methods tosend, update and delete the given messsage 2. Added test to check for message publish using REST API --- .../io/ably/lib/chat/ChatMessagesTest.java | 86 +++++++++++++++++++ .../test/java/io/ably/lib/chat/ChatRoom.java | 62 +++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java create mode 100644 lib/src/test/java/io/ably/lib/chat/ChatRoom.java diff --git a/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java b/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java new file mode 100644 index 000000000..5f3b90ea1 --- /dev/null +++ b/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java @@ -0,0 +1,86 @@ +package io.ably.lib.chat; + +import com.google.gson.JsonObject; +import io.ably.lib.realtime.AblyRealtime; +import io.ably.lib.realtime.Channel; +import io.ably.lib.realtime.ChannelState; +import io.ably.lib.test.common.Helpers; +import io.ably.lib.test.common.ParameterizedTest; +import io.ably.lib.types.ClientOptions; +import io.ably.lib.types.Message; +import io.ably.lib.types.MessageAction; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class ChatMessagesTest extends ParameterizedTest { + /** + * Connect to the service and attach, then subscribe and unsubscribe + */ + @Test + public void test_room_message_is_published() { + String roomId = "1234"; + String channelName = roomId + "::$chat::$chatMessages"; + AblyRealtime ably = null; + try { + ClientOptions opts = createOptions(testVars.keys[7].keyStr); + opts.clientId = "sandbox-client"; + ably = new AblyRealtime(opts); + ChatRoom room = new ChatRoom(roomId, ably); + + /* create a channel and attach */ + final Channel channel = ably.channels.get(channelName); + channel.attach(); + (new Helpers.ChannelWaiter(channel)).waitFor(ChannelState.attached); + + /* subscribe to messages */ + List receivedMsg = new ArrayList<>(); + channel.subscribe(receivedMsg::add); + + // send message to room + ChatRoom.SendMessageParams params = new ChatRoom.SendMessageParams(); + params.text = "hello there"; + JsonObject sendMessageResult = (JsonObject) room.sendMessage(params); + // check sendMessageResult has 2 fields and are not null + Assert.assertEquals(2, sendMessageResult.entrySet().size()); + String resultSerial = sendMessageResult.get("serial").getAsString(); + Assert.assertFalse(resultSerial.isEmpty()); + String resultCreatedAt = sendMessageResult.get("createdAt").getAsString(); + Assert.assertFalse(resultCreatedAt.isEmpty()); + + Exception err = new Helpers.ConditionalWaiter().wait(() -> !receivedMsg.isEmpty(), 10_000); + Assert.assertNull(err); + + Assert.assertEquals(1, receivedMsg.size()); + Message message = receivedMsg.get(0); + + Assert.assertFalse("Message ID should not be empty", message.id.isEmpty()); + Assert.assertEquals("chat.message", message.name); + Assert.assertEquals("sandbox-client", message.clientId); + + JsonObject data = (JsonObject) message.data; + // has two fields "text" and "metadata" + Assert.assertEquals(2, data.entrySet().size()); + Assert.assertEquals("hello there", data.get("text").getAsString()); + Assert.assertTrue(data.get("metadata").isJsonObject()); + + Assert.assertEquals(resultCreatedAt, String.valueOf(message.timestamp)); + + Assert.assertEquals(resultCreatedAt, message.createdAt.toString()); + Assert.assertEquals(resultSerial, message.serial); + Assert.assertEquals(resultSerial, message.version); + + Assert.assertEquals(MessageAction.MESSAGE_CREATE, message.action); + Assert.assertEquals(resultCreatedAt, message.createdAt.toString()); + + } catch (Exception e) { + e.printStackTrace(); + Assert.fail("init0: Unexpected exception instantiating library"); + } finally { + if(ably != null) + ably.close(); + } + } +} diff --git a/lib/src/test/java/io/ably/lib/chat/ChatRoom.java b/lib/src/test/java/io/ably/lib/chat/ChatRoom.java new file mode 100644 index 000000000..8efb2dd3f --- /dev/null +++ b/lib/src/test/java/io/ably/lib/chat/ChatRoom.java @@ -0,0 +1,62 @@ +package io.ably.lib.chat; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import io.ably.lib.http.HttpCore; +import io.ably.lib.http.HttpUtils; +import io.ably.lib.rest.AblyRest; +import io.ably.lib.types.AblyException; +import io.ably.lib.types.HttpPaginatedResponse; +import io.ably.lib.types.Param; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; + +public class ChatRoom { + private final AblyRest ablyRest; + private final String roomId; + + protected ChatRoom(String roomId, AblyRest ablyRest) { + this.roomId = roomId; + this.ablyRest = ablyRest; + } + + public JsonElement sendMessage(SendMessageParams params) throws Exception { + return makeAuthorizedRequest("/chat/v2/rooms/" + roomId + "/messages", "POST", new Gson().toJsonTree(params)) + .orElseThrow(() -> new Exception("Failed to send message")); + } + + public JsonElement updateMessage(String serial, UpdateMessageParams params) throws Exception { + return makeAuthorizedRequest("/chat/v2/rooms/" + roomId + "/messages/" + serial, "PUT", new Gson().toJsonTree(params)) + .orElseThrow(() -> new Exception("Failed to update message")); + } + + public JsonElement deleteMessage(String serial, DeleteMessageParams params) throws Exception { + return makeAuthorizedRequest("/chat/v2/rooms/" + roomId + "/messages/" + serial + "/delete", "POST", new Gson().toJsonTree(params)) + .orElseThrow(() -> new Exception("Failed to delete message")); + } + + public static class SendMessageParams { + public String text; + public Map metadata; + public Map headers; + } + + public static class UpdateMessageParams { + public SendMessageParams message; + public String description; + public Map metadata; + } + + public static class DeleteMessageParams { + public String description; + public Map metadata; + } + + protected Optional makeAuthorizedRequest(String url, String method, JsonElement body) throws AblyException { + HttpCore.RequestBody httpRequestBody = HttpUtils.requestBodyFromGson(body, ablyRest.options.useBinaryProtocol); + HttpPaginatedResponse response = ablyRest.request(method, url, new Param[] { new Param("v", 3) }, httpRequestBody, null); + return Arrays.stream(response.items()).findFirst(); + } +} From 54593dd20b36c5a7601a06c5dbae86fd9d5cb3a7 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Mon, 20 Jan 2025 17:37:21 +0530 Subject: [PATCH 04/10] [ECO-5193][TM*] Updated Message.java 1. Changed metadata type from Map to JsonObject, 2. Updated relevant tests, added missing assertions --- .../main/java/io/ably/lib/types/Message.java | 20 +++++-------- .../io/ably/lib/chat/ChatMessagesTest.java | 29 +++++++++++++++++-- .../test/java/io/ably/lib/chat/ChatRoom.java | 7 +++-- .../java/io/ably/lib/types/MessageTest.java | 21 +++++++------- 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/lib/src/main/java/io/ably/lib/types/Message.java b/lib/src/main/java/io/ably/lib/types/Message.java index 463a361bb..36d36df8a 100644 --- a/lib/src/main/java/io/ably/lib/types/Message.java +++ b/lib/src/main/java/io/ably/lib/types/Message.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.lang.reflect.Type; import java.util.Collection; -import java.util.HashMap; import java.util.Map; import com.google.gson.JsonArray; @@ -21,6 +20,7 @@ import io.ably.lib.util.Log; + /** * Contains an individual message that is sent to, or received from, Ably. */ @@ -95,7 +95,7 @@ public class Message extends BaseMessage { public static class Operation { public String clientId; public String description; - public Map metadata; + public JsonObject metadata; void write(MessagePacker packer) throws IOException { packer.packMapHeader(3); @@ -110,9 +110,9 @@ void write(MessagePacker packer) throws IOException { if(metadata != null) { packer.packString("metadata"); packer.packMapHeader(metadata.size()); - for(Map.Entry entry : metadata.entrySet()) { + for(Map.Entry entry : metadata.entrySet()) { packer.packString(entry.getKey()); - packer.packString(entry.getValue()); + Serialisation.gsonToMsgpack(entry.getValue(), packer); } } } @@ -131,11 +131,11 @@ protected static Operation read(final MessageUnpacker unpacker) throws IOExcepti break; case "metadata": int mapSize = unpacker.unpackMapHeader(); - operation.metadata = new HashMap<>(mapSize); + operation.metadata = new JsonObject(); for (int j = 0; j < mapSize; j++) { String key = unpacker.unpackString(); - String value = unpacker.unpackString(); - operation.metadata.put(key, value); + JsonElement value = Serialisation.msgpackToGson(unpacker.unpackValue()); + operation.metadata.add(key, value); } break; default: @@ -155,11 +155,7 @@ protected static Operation read(final JsonObject jsonObject) throws MessageDecod operation.description = jsonObject.get("description").getAsString(); } if (jsonObject.has("metadata")) { - JsonObject metadataObject = jsonObject.getAsJsonObject("metadata"); - operation.metadata = new HashMap<>(); - for (Map.Entry entry : metadataObject.entrySet()) { - operation.metadata.put(entry.getKey(), entry.getValue().getAsString()); - } + operation.metadata = jsonObject.getAsJsonObject("metadata"); } return operation; } diff --git a/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java b/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java index 5f3b90ea1..1dd1327a7 100644 --- a/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java +++ b/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java @@ -13,11 +13,14 @@ import org.junit.Test; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class ChatMessagesTest extends ParameterizedTest { /** - * Connect to the service and attach, then subscribe and unsubscribe + * Test that a message sent via rest API is sent to a messages channel. + * It should be received by the client that is subscribed to the messages channel. */ @Test public void test_room_message_is_published() { @@ -42,6 +45,15 @@ public void test_room_message_is_published() { // send message to room ChatRoom.SendMessageParams params = new ChatRoom.SendMessageParams(); params.text = "hello there"; + params.metadata = new JsonObject(); + JsonObject foo = new JsonObject(); + foo.addProperty("bar", 1); + params.metadata.add("foo", foo); + Map headers = new HashMap<>(); + headers.put("header1", "value1"); + headers.put("baz", "qux"); + params.headers = headers; + JsonObject sendMessageResult = (JsonObject) room.sendMessage(params); // check sendMessageResult has 2 fields and are not null Assert.assertEquals(2, sendMessageResult.entrySet().size()); @@ -63,8 +75,21 @@ public void test_room_message_is_published() { JsonObject data = (JsonObject) message.data; // has two fields "text" and "metadata" Assert.assertEquals(2, data.entrySet().size()); + // Assert for received text Assert.assertEquals("hello there", data.get("text").getAsString()); - Assert.assertTrue(data.get("metadata").isJsonObject()); + // Assert on received metadata + JsonObject metadata = data.getAsJsonObject("metadata"); + Assert.assertTrue(metadata.has("foo")); + Assert.assertTrue(metadata.get("foo").isJsonObject()); + Assert.assertEquals(1, metadata.getAsJsonObject("foo").get("bar").getAsInt()); + + // Assert sent headers as a part of message.extras.headers + JsonObject extrasJson = message.extras.asJsonObject(); + Assert.assertTrue(extrasJson.has("headers")); + JsonObject headersJson = extrasJson.getAsJsonObject("headers"); + Assert.assertEquals(2, headersJson.entrySet().size()); + Assert.assertEquals("value1", headersJson.get("header1").getAsString()); + Assert.assertEquals("qux", headersJson.get("baz").getAsString()); Assert.assertEquals(resultCreatedAt, String.valueOf(message.timestamp)); diff --git a/lib/src/test/java/io/ably/lib/chat/ChatRoom.java b/lib/src/test/java/io/ably/lib/chat/ChatRoom.java index 8efb2dd3f..26875413e 100644 --- a/lib/src/test/java/io/ably/lib/chat/ChatRoom.java +++ b/lib/src/test/java/io/ably/lib/chat/ChatRoom.java @@ -2,6 +2,7 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import io.ably.lib.http.HttpCore; import io.ably.lib.http.HttpUtils; import io.ably.lib.rest.AblyRest; @@ -39,19 +40,19 @@ public JsonElement deleteMessage(String serial, DeleteMessageParams params) thro public static class SendMessageParams { public String text; - public Map metadata; + public JsonObject metadata; public Map headers; } public static class UpdateMessageParams { public SendMessageParams message; public String description; - public Map metadata; + public JsonObject metadata; } public static class DeleteMessageParams { public String description; - public Map metadata; + public JsonObject metadata; } protected Optional makeAuthorizedRequest(String url, String method, JsonElement body) throws AblyException { diff --git a/lib/src/test/java/io/ably/lib/types/MessageTest.java b/lib/src/test/java/io/ably/lib/types/MessageTest.java index 3a7997725..5c957d3bb 100644 --- a/lib/src/test/java/io/ably/lib/types/MessageTest.java +++ b/lib/src/test/java/io/ably/lib/types/MessageTest.java @@ -12,7 +12,6 @@ import org.msgpack.core.MessageUnpacker; import java.io.ByteArrayOutputStream; -import java.util.HashMap; public class MessageTest { @@ -107,9 +106,9 @@ public void serialize_message_with_operation() { Message.Operation operation = new Message.Operation(); operation.clientId = "operation-client-id"; operation.description = "operation-description"; - operation.metadata = new HashMap<>(); - operation.metadata.put("key1", "value1"); - operation.metadata.put("key2", "value2"); + operation.metadata = new JsonObject(); + operation.metadata.addProperty("key1", "value1"); + operation.metadata.addProperty("key2", "value2"); message.operation = operation; // When @@ -162,8 +161,8 @@ public void deserialize_message_with_operation() throws Exception { assertEquals("test-key", message.connectionKey); assertEquals("operation-client-id", message.operation.clientId); assertEquals("operation-description", message.operation.description); - assertEquals("value1", message.operation.metadata.get("key1")); - assertEquals("value2", message.operation.metadata.get("key2")); + assertEquals("value1", message.operation.metadata.get("key1").getAsString()); + assertEquals("value2", message.operation.metadata.get("key2").getAsString()); } @Test @@ -200,9 +199,9 @@ public void serialize_and_deserialize_with_msgpack() throws Exception { Message.Operation operation = new Message.Operation(); operation.clientId = "operation-client-id"; operation.description = "operation-description"; - operation.metadata = new HashMap<>(); - operation.metadata.put("key1", "value1"); - operation.metadata.put("key2", "value2"); + operation.metadata = new JsonObject(); + operation.metadata.addProperty("key1", "value1"); + operation.metadata.addProperty("key2", "value2"); message.operation = operation; // When Encode to MessagePack @@ -227,7 +226,7 @@ public void serialize_and_deserialize_with_msgpack() throws Exception { assertEquals("01826232498871-001@abcdefghij:001", unpacked.serial); assertEquals("operation-client-id", unpacked.operation.clientId); assertEquals("operation-description", unpacked.operation.description); - assertEquals("value1", unpacked.operation.metadata.get("key1")); - assertEquals("value2", unpacked.operation.metadata.get("key2")); + assertEquals("value1", unpacked.operation.metadata.get("key1").getAsString()); + assertEquals("value2", unpacked.operation.metadata.get("key2").getAsString()); } } From 172b5747b08cf6784f20ecb37c15d7e020e61e6c Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Mon, 20 Jan 2025 19:33:03 +0530 Subject: [PATCH 05/10] [ECO-5193][TM*] Updated ChaneMessagesTest.java 1. Added test to check for updated room message --- .../io/ably/lib/chat/ChatMessagesTest.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java b/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java index 1dd1327a7..bd3e39802 100644 --- a/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java +++ b/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java @@ -108,4 +108,118 @@ public void test_room_message_is_published() { ably.close(); } } + + /** + * Test that a message updated via rest API is sent to a messages channel. + * It should be received by another client that is subscribed to the same messages channel. + * Make sure to use two clientIds: clientId1 and clientId2 + */ + @Test + public void test_room_message_is_updated() { + String roomId = "1234"; + String channelName = roomId + "::$chat::$chatMessages"; + AblyRealtime ablyClient1 = null; + AblyRealtime ablyClient2 = null; + try { + ClientOptions opts1 = createOptions(testVars.keys[7].keyStr); + opts1.clientId = "clientId1"; + ablyClient1 = new AblyRealtime(opts1); + + ClientOptions opts2 = createOptions(testVars.keys[7].keyStr); + opts2.clientId = "clientId2"; + ablyClient2 = new AblyRealtime(opts2); + + ChatRoom room = new ChatRoom(roomId, ablyClient1); + + // Create a channel and attach with client1 + final Channel channel1 = ablyClient1.channels.get(channelName); + channel1.attach(); + (new Helpers.ChannelWaiter(channel1)).waitFor(ChannelState.attached); + + // Subscribe to messages with client2 + final Channel channel2 = ablyClient2.channels.get(channelName); + channel2.attach(); + (new Helpers.ChannelWaiter(channel2)).waitFor(ChannelState.attached); + + List receivedMsg = new ArrayList<>(); + channel2.subscribe(receivedMsg::add); + + // Send message to room + ChatRoom.SendMessageParams params = new ChatRoom.SendMessageParams(); + params.text = "hello there"; + JsonObject sendMessageResult = (JsonObject) room.sendMessage(params); + String originalSerial = sendMessageResult.get("serial").getAsString(); + String originalCreatedAt = sendMessageResult.get("createdAt").getAsString(); + + // Wait for the message to be received + Exception err = new Helpers.ConditionalWaiter().wait(() -> !receivedMsg.isEmpty(), 10_000); + Assert.assertNull(err); + + // Update the message + ChatRoom.UpdateMessageParams updateParams = new ChatRoom.UpdateMessageParams(); + // Update message context + updateParams.message = new ChatRoom.SendMessageParams(); + updateParams.message.text = "updated text"; + JsonObject metaData = new JsonObject(); + JsonObject foo = new JsonObject(); + foo.addProperty("bar", 1); + metaData.add("foo", foo); + updateParams.message.metadata = metaData; + // Update description + updateParams.description = "message updated by clientId1"; + + // TODO - Update external metadata, this will be populated in operation field + // updateParams.metadata = params.metadata; + + JsonObject updateMessageResult = (JsonObject) room.updateMessage(originalSerial, updateParams); + String updateResultVersion = updateMessageResult.get("version").getAsString(); + String updateResultTimestamp = updateMessageResult.get("timestamp").getAsString(); + + // Wait for the updated message to be received + err = new Helpers.ConditionalWaiter().wait(() -> receivedMsg.size() == 2, 10_000); + Assert.assertNull(err); + + // Verify the updated message + Message updatedMessage = receivedMsg.get(1); + + Assert.assertEquals(MessageAction.MESSAGE_UPDATE, updatedMessage.action); + + Assert.assertFalse("Message ID should not be empty", updatedMessage.id.isEmpty()); + Assert.assertEquals("chat.message", updatedMessage.name); + Assert.assertEquals("clientId1", updatedMessage.clientId); + + JsonObject data = (JsonObject) updatedMessage.data; + Assert.assertEquals(2, data.entrySet().size()); + Assert.assertEquals("updated text", data.get("text").getAsString()); + JsonObject metadata = data.getAsJsonObject("metadata"); + Assert.assertTrue(metadata.has("foo")); + Assert.assertTrue(metadata.get("foo").isJsonObject()); + Assert.assertEquals(1, metadata.getAsJsonObject("foo").get("bar").getAsInt()); + + Assert.assertEquals(originalSerial, updatedMessage.serial); + Assert.assertEquals(updateResultVersion, updatedMessage.version); + + Assert.assertEquals(originalCreatedAt, updatedMessage.createdAt.toString()); + Assert.assertEquals(updateResultTimestamp, String.valueOf(updatedMessage.timestamp)); + + // TODO - Add assertion for operation field + + } catch (Exception e) { + e.printStackTrace(); + Assert.fail("Unexpected exception instantiating library"); + } finally { + if (ablyClient1 != null) ablyClient1.close(); + if (ablyClient2 != null) ablyClient2.close(); + } + } + + /** + * Test that a message deleted via rest API is sent to a messages channel. + * It should be received by another client that is subscribed to the same messages channel. + * Make sure to use two clientIds: clientId1 and clientId2 + */ + @Test + public void test_room_message_is_deleted() { + + } } From 5ce3ed8ba43f18d37cb5c0340cc5fd7fb1fe5d35 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 21 Jan 2025 18:40:22 +0530 Subject: [PATCH 06/10] [ECO-5193][TM*] Updated Message.java 1. Reverted operation metadata to hashmap 2. Updated relevant tests for the same --- .../main/java/io/ably/lib/types/Message.java | 20 ++++++----- .../test/java/io/ably/lib/chat/ChatRoom.java | 4 +-- .../java/io/ably/lib/types/MessageTest.java | 33 ++++++++++--------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/lib/src/main/java/io/ably/lib/types/Message.java b/lib/src/main/java/io/ably/lib/types/Message.java index 36d36df8a..463a361bb 100644 --- a/lib/src/main/java/io/ably/lib/types/Message.java +++ b/lib/src/main/java/io/ably/lib/types/Message.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.lang.reflect.Type; import java.util.Collection; +import java.util.HashMap; import java.util.Map; import com.google.gson.JsonArray; @@ -20,7 +21,6 @@ import io.ably.lib.util.Log; - /** * Contains an individual message that is sent to, or received from, Ably. */ @@ -95,7 +95,7 @@ public class Message extends BaseMessage { public static class Operation { public String clientId; public String description; - public JsonObject metadata; + public Map metadata; void write(MessagePacker packer) throws IOException { packer.packMapHeader(3); @@ -110,9 +110,9 @@ void write(MessagePacker packer) throws IOException { if(metadata != null) { packer.packString("metadata"); packer.packMapHeader(metadata.size()); - for(Map.Entry entry : metadata.entrySet()) { + for(Map.Entry entry : metadata.entrySet()) { packer.packString(entry.getKey()); - Serialisation.gsonToMsgpack(entry.getValue(), packer); + packer.packString(entry.getValue()); } } } @@ -131,11 +131,11 @@ protected static Operation read(final MessageUnpacker unpacker) throws IOExcepti break; case "metadata": int mapSize = unpacker.unpackMapHeader(); - operation.metadata = new JsonObject(); + operation.metadata = new HashMap<>(mapSize); for (int j = 0; j < mapSize; j++) { String key = unpacker.unpackString(); - JsonElement value = Serialisation.msgpackToGson(unpacker.unpackValue()); - operation.metadata.add(key, value); + String value = unpacker.unpackString(); + operation.metadata.put(key, value); } break; default: @@ -155,7 +155,11 @@ protected static Operation read(final JsonObject jsonObject) throws MessageDecod operation.description = jsonObject.get("description").getAsString(); } if (jsonObject.has("metadata")) { - operation.metadata = jsonObject.getAsJsonObject("metadata"); + JsonObject metadataObject = jsonObject.getAsJsonObject("metadata"); + operation.metadata = new HashMap<>(); + for (Map.Entry entry : metadataObject.entrySet()) { + operation.metadata.put(entry.getKey(), entry.getValue().getAsString()); + } } return operation; } diff --git a/lib/src/test/java/io/ably/lib/chat/ChatRoom.java b/lib/src/test/java/io/ably/lib/chat/ChatRoom.java index 26875413e..316c21098 100644 --- a/lib/src/test/java/io/ably/lib/chat/ChatRoom.java +++ b/lib/src/test/java/io/ably/lib/chat/ChatRoom.java @@ -47,12 +47,12 @@ public static class SendMessageParams { public static class UpdateMessageParams { public SendMessageParams message; public String description; - public JsonObject metadata; + public Map metadata; } public static class DeleteMessageParams { public String description; - public JsonObject metadata; + public Map metadata; } protected Optional makeAuthorizedRequest(String url, String method, JsonElement body) throws AblyException { diff --git a/lib/src/test/java/io/ably/lib/types/MessageTest.java b/lib/src/test/java/io/ably/lib/types/MessageTest.java index 5c957d3bb..18dcf81d7 100644 --- a/lib/src/test/java/io/ably/lib/types/MessageTest.java +++ b/lib/src/test/java/io/ably/lib/types/MessageTest.java @@ -12,6 +12,7 @@ import org.msgpack.core.MessageUnpacker; import java.io.ByteArrayOutputStream; +import java.util.HashMap; public class MessageTest { @@ -77,12 +78,12 @@ public void serialize_message_with_serial() { @Test public void deserialize_message_with_serial() throws Exception { // Given - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("clientId", "test-client-id"); - jsonObject.addProperty("data", "test-data"); - jsonObject.addProperty("name", "test-name"); - jsonObject.addProperty("action", 0); - jsonObject.addProperty("serial", "01826232498871-001@abcdefghij:001"); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("clientId", "test-client-id"); + jsonObject.addProperty("data", "test-data"); + jsonObject.addProperty("name", "test-name"); + jsonObject.addProperty("action", 0); + jsonObject.addProperty("serial", "01826232498871-001@abcdefghij:001"); // When Message message = Message.fromEncoded(jsonObject, new ChannelOptions()); @@ -106,9 +107,9 @@ public void serialize_message_with_operation() { Message.Operation operation = new Message.Operation(); operation.clientId = "operation-client-id"; operation.description = "operation-description"; - operation.metadata = new JsonObject(); - operation.metadata.addProperty("key1", "value1"); - operation.metadata.addProperty("key2", "value2"); + operation.metadata = new HashMap<>(); + operation.metadata.put("key1", "value1"); + operation.metadata.put("key2", "value2"); message.operation = operation; // When @@ -161,8 +162,8 @@ public void deserialize_message_with_operation() throws Exception { assertEquals("test-key", message.connectionKey); assertEquals("operation-client-id", message.operation.clientId); assertEquals("operation-description", message.operation.description); - assertEquals("value1", message.operation.metadata.get("key1").getAsString()); - assertEquals("value2", message.operation.metadata.get("key2").getAsString()); + assertEquals("value1", message.operation.metadata.get("key1")); + assertEquals("value2", message.operation.metadata.get("key2")); } @Test @@ -199,9 +200,9 @@ public void serialize_and_deserialize_with_msgpack() throws Exception { Message.Operation operation = new Message.Operation(); operation.clientId = "operation-client-id"; operation.description = "operation-description"; - operation.metadata = new JsonObject(); - operation.metadata.addProperty("key1", "value1"); - operation.metadata.addProperty("key2", "value2"); + operation.metadata = new HashMap<>(); + operation.metadata.put("key1", "value1"); + operation.metadata.put("key2", "value2"); message.operation = operation; // When Encode to MessagePack @@ -226,7 +227,7 @@ public void serialize_and_deserialize_with_msgpack() throws Exception { assertEquals("01826232498871-001@abcdefghij:001", unpacked.serial); assertEquals("operation-client-id", unpacked.operation.clientId); assertEquals("operation-description", unpacked.operation.description); - assertEquals("value1", unpacked.operation.metadata.get("key1").getAsString()); - assertEquals("value2", unpacked.operation.metadata.get("key2").getAsString()); + assertEquals("value1", unpacked.operation.metadata.get("key1")); + assertEquals("value2", unpacked.operation.metadata.get("key2")); } } From 0cccdce9c211b1c0e6aba4bcab155a53983d962b Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 21 Jan 2025 18:47:11 +0530 Subject: [PATCH 07/10] [ECO-5193][TM*] Updated ChaneMessagesTest.java - Updated assertions for test_room_message_is_updated test - Added assertions to check if operation field is populated properly with clientId, description and metadata --- .../io/ably/lib/chat/ChatMessagesTest.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java b/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java index bd3e39802..6e568c20e 100644 --- a/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java +++ b/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java @@ -168,8 +168,11 @@ public void test_room_message_is_updated() { // Update description updateParams.description = "message updated by clientId1"; - // TODO - Update external metadata, this will be populated in operation field - // updateParams.metadata = params.metadata; + // Update metadata, add few random fields + Map operationMetadata = new HashMap<>(); + operationMetadata.put("foo", "bar"); + operationMetadata.put("naruto", "hero"); + updateParams.metadata = operationMetadata; JsonObject updateMessageResult = (JsonObject) room.updateMessage(originalSerial, updateParams); String updateResultVersion = updateMessageResult.get("version").getAsString(); @@ -197,12 +200,18 @@ public void test_room_message_is_updated() { Assert.assertEquals(1, metadata.getAsJsonObject("foo").get("bar").getAsInt()); Assert.assertEquals(originalSerial, updatedMessage.serial); - Assert.assertEquals(updateResultVersion, updatedMessage.version); - Assert.assertEquals(originalCreatedAt, updatedMessage.createdAt.toString()); + + Assert.assertEquals(updateResultVersion, updatedMessage.version); Assert.assertEquals(updateResultTimestamp, String.valueOf(updatedMessage.timestamp)); - // TODO - Add assertion for operation field + // updatedMessage contains `operation` with fields as clientId, description, metadata, assert for these fields + Message.Operation operation = updatedMessage.operation; + Assert.assertEquals("clientId1", operation.clientId); + Assert.assertEquals("message updated by clientId1", operation.description); + Assert.assertEquals(2, operation.metadata.size()); + Assert.assertEquals("bar", operation.metadata.get("foo")); + Assert.assertEquals("hero", operation.metadata.get("naruto")); } catch (Exception e) { e.printStackTrace(); From e8f3f87a4b5ddc4fd1be25483357986e01de9ec4 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 21 Jan 2025 20:09:15 +0530 Subject: [PATCH 08/10] [ECO-5193][TM*] Updated ChaneMessagesTest.java, implemented message delete test --- .../io/ably/lib/chat/ChatMessagesTest.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java b/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java index 6e568c20e..c26b4dbd0 100644 --- a/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java +++ b/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java @@ -229,6 +229,91 @@ public void test_room_message_is_updated() { */ @Test public void test_room_message_is_deleted() { + String roomId = "1234"; + String channelName = roomId + "::$chat::$chatMessages"; + AblyRealtime ablyClient1 = null; + AblyRealtime ablyClient2 = null; + try { + ClientOptions opts1 = createOptions(testVars.keys[7].keyStr); + opts1.clientId = "clientId1"; + ablyClient1 = new AblyRealtime(opts1); + + ClientOptions opts2 = createOptions(testVars.keys[7].keyStr); + opts2.clientId = "clientId2"; + ablyClient2 = new AblyRealtime(opts2); + + ChatRoom room = new ChatRoom(roomId, ablyClient1); + + // Create a channel and attach with client1 + final Channel channel1 = ablyClient1.channels.get(channelName); + channel1.attach(); + (new Helpers.ChannelWaiter(channel1)).waitFor(ChannelState.attached); + + // Subscribe to messages with client2 + final Channel channel2 = ablyClient2.channels.get(channelName); + channel2.attach(); + (new Helpers.ChannelWaiter(channel2)).waitFor(ChannelState.attached); + + List receivedMsg = new ArrayList<>(); + channel2.subscribe(receivedMsg::add); + + // Send message to room + ChatRoom.SendMessageParams params = new ChatRoom.SendMessageParams(); + params.text = "hello there"; + JsonObject sendMessageResult = (JsonObject) room.sendMessage(params); + String originalSerial = sendMessageResult.get("serial").getAsString(); + String originalCreatedAt = sendMessageResult.get("createdAt").getAsString(); + + // Wait for the message to be received + Exception err = new Helpers.ConditionalWaiter().wait(() -> !receivedMsg.isEmpty(), 10_000); + Assert.assertNull(err); + + // Delete the message + ChatRoom.DeleteMessageParams deleteParams = new ChatRoom.DeleteMessageParams(); + deleteParams.description = "message deleted by clientId1"; + Map deleteMetadata = new HashMap<>(); + deleteMetadata.put("foo", "bar"); + deleteMetadata.put("naruto", "hero"); + deleteParams.metadata = deleteMetadata; + + JsonObject deleteMessageResult = (JsonObject) room.deleteMessage(originalSerial, deleteParams); + String deleteResultVersion = deleteMessageResult.get("version").getAsString(); + String deleteResultTimestamp = deleteMessageResult.get("timestamp").getAsString(); + + // Wait for the deleted message to be received + err = new Helpers.ConditionalWaiter().wait(() -> receivedMsg.size() == 2, 10_000); + Assert.assertNull(err); + + // Verify the deleted message + Message deletedMessage = receivedMsg.get(1); + + Assert.assertEquals(MessageAction.MESSAGE_DELETE, deletedMessage.action); + Assert.assertFalse("Message ID should not be empty", deletedMessage.id.isEmpty()); + Assert.assertEquals("chat.message", deletedMessage.name); + Assert.assertEquals("clientId1", deletedMessage.clientId); + + Assert.assertEquals(originalSerial, deletedMessage.serial); + Assert.assertEquals(originalCreatedAt, deletedMessage.createdAt.toString()); + + Assert.assertEquals(deleteResultVersion, deletedMessage.version); + Assert.assertEquals(deleteResultTimestamp, String.valueOf(deletedMessage.timestamp)); + + // deletedMessage contains `operation` with fields as clientId, reason + Message.Operation operation = deletedMessage.operation; + Assert.assertEquals("clientId1", operation.clientId); + Assert.assertEquals("message deleted by clientId1", operation.description); + // assert on metadata + Assert.assertEquals(2, operation.metadata.size()); + Assert.assertEquals("bar", operation.metadata.get("foo")); + Assert.assertEquals("hero", operation.metadata.get("naruto")); + + } catch (Exception e) { + e.printStackTrace(); + Assert.fail("Unexpected exception instantiating library"); + } finally { + if (ablyClient1 != null) ablyClient1.close(); + if (ablyClient2 != null) ablyClient2.close(); + } } } From 4610d4d86aa18cfc8f36e1161693a77f49101b1f Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 21 Jan 2025 20:31:53 +0530 Subject: [PATCH 09/10] [ECO-5193][TM*] Updated ChaneMessagesTest.java 1. Implemented integration test for message create, update and delete serially. 2. Implemented integration test to check for allowed ops on deleted message. --- .../io/ably/lib/chat/ChatMessagesTest.java | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java b/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java index c26b4dbd0..940071a75 100644 --- a/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java +++ b/lib/src/test/java/io/ably/lib/chat/ChatMessagesTest.java @@ -316,4 +316,208 @@ public void test_room_message_is_deleted() { if (ablyClient2 != null) ablyClient2.close(); } } + + /** + * Test that message is created, updated and then deleted serially + */ + @Test + public void test_room_message_create_update_delete() { + String roomId = "1234"; + String channelName = roomId + "::$chat::$chatMessages"; + AblyRealtime ablyClient1 = null; + AblyRealtime ablyClient2 = null; + try { + ClientOptions opts1 = createOptions(testVars.keys[7].keyStr); + opts1.clientId = "clientId1"; + ablyClient1 = new AblyRealtime(opts1); + + ClientOptions opts2 = createOptions(testVars.keys[7].keyStr); + opts2.clientId = "clientId2"; + ablyClient2 = new AblyRealtime(opts2); + + ChatRoom room = new ChatRoom(roomId, ablyClient1); + + // Create a channel and attach with client1 + final Channel channel1 = ablyClient1.channels.get(channelName); + channel1.attach(); + (new Helpers.ChannelWaiter(channel1)).waitFor(ChannelState.attached); + + // Subscribe to messages with client2 + final Channel channel2 = ablyClient2.channels.get(channelName); + channel2.attach(); + (new Helpers.ChannelWaiter(channel2)).waitFor(ChannelState.attached); + + List receivedMsg = new ArrayList<>(); + channel2.subscribe(receivedMsg::add); + + // Send message to room + ChatRoom.SendMessageParams sendParams = new ChatRoom.SendMessageParams(); + sendParams.text = "hello there"; + + JsonObject sendMessageResult = (JsonObject) room.sendMessage(sendParams); + String originalSerial = sendMessageResult.get("serial").getAsString(); + String originalCreatedAt = sendMessageResult.get("createdAt").getAsString(); + + // Wait for the message to be received + Exception err = new Helpers.ConditionalWaiter().wait(() -> !receivedMsg.isEmpty(), 10_000); + Assert.assertNull(err); + + // Update the message + ChatRoom.UpdateMessageParams updateParams = new ChatRoom.UpdateMessageParams(); + updateParams.message = new ChatRoom.SendMessageParams(); + updateParams.message.text = "updated text"; + + JsonObject updateMessageResult = (JsonObject) room.updateMessage(originalSerial, updateParams); + String updateResultVersion = updateMessageResult.get("version").getAsString(); + String updateResultTimestamp = updateMessageResult.get("timestamp").getAsString(); + + // Wait for the updated message to be received + err = new Helpers.ConditionalWaiter().wait(() -> receivedMsg.size() == 2, 10_000); + Assert.assertNull(err); + + // Delete the message + ChatRoom.DeleteMessageParams deleteParams = new ChatRoom.DeleteMessageParams(); + deleteParams.description = "message deleted by clientId1"; + + JsonObject deleteMessageResult = (JsonObject) room.deleteMessage(originalSerial, deleteParams); + String deleteResultVersion = deleteMessageResult.get("version").getAsString(); + String deleteResultTimestamp = deleteMessageResult.get("timestamp").getAsString(); + + // Wait for the deleted message to be received + err = new Helpers.ConditionalWaiter().wait(() -> receivedMsg.size() == 3, 10_000); + Assert.assertNull(err); + + // Verify the created message + Message createdMessage = receivedMsg.get(0); + Assert.assertEquals(MessageAction.MESSAGE_CREATE, createdMessage.action); + Assert.assertFalse("Message ID should not be empty", createdMessage.id.isEmpty()); + Assert.assertEquals("chat.message", createdMessage.name); + Assert.assertEquals("clientId1", createdMessage.clientId); + JsonObject createdData = (JsonObject) createdMessage.data; + Assert.assertEquals("hello there", createdData.get("text").getAsString()); + + // Verify the updated message + Message updatedMessage = receivedMsg.get(1); + Assert.assertEquals(MessageAction.MESSAGE_UPDATE, updatedMessage.action); + Assert.assertFalse("Message ID should not be empty", updatedMessage.id.isEmpty()); + Assert.assertEquals("chat.message", updatedMessage.name); + Assert.assertEquals("clientId1", updatedMessage.clientId); + JsonObject updatedData = (JsonObject) updatedMessage.data; + Assert.assertEquals("updated text", updatedData.get("text").getAsString()); + + Assert.assertEquals(updateResultVersion, updatedMessage.version); + Assert.assertEquals(updateResultTimestamp, String.valueOf(updatedMessage.timestamp)); + + // Verify the deleted message + Message deletedMessage = receivedMsg.get(2); + Assert.assertEquals(MessageAction.MESSAGE_DELETE, deletedMessage.action); + Assert.assertFalse("Message ID should not be empty", deletedMessage.id.isEmpty()); + Assert.assertEquals("chat.message", deletedMessage.name); + Assert.assertEquals("clientId1", deletedMessage.clientId); + + Assert.assertEquals(deleteResultVersion, deletedMessage.version); + Assert.assertEquals(deleteResultTimestamp, String.valueOf(deletedMessage.timestamp)); + + // Check original serials + Assert.assertEquals(originalSerial, createdMessage.serial); + Assert.assertEquals(originalSerial, updatedMessage.serial); + Assert.assertEquals(originalSerial, deletedMessage.serial); + + // Check original message createdAt + Assert.assertEquals(originalCreatedAt, createdMessage.createdAt.toString()); + Assert.assertEquals(originalCreatedAt, updatedMessage.createdAt.toString()); + Assert.assertEquals(originalCreatedAt, deletedMessage.createdAt.toString()); + + } catch (Exception e) { + e.printStackTrace(); + Assert.fail("Unexpected exception instantiating library"); + } finally { + if (ablyClient1 != null) ablyClient1.close(); + if (ablyClient2 != null) ablyClient2.close(); + } + } + + /** + * Test that update/delete operations are allowed on a deleted message. + */ + @Test + public void test_operations_allowed_on_deleted_message() { + String roomId = "1234"; + String channelName = roomId + "::$chat::$chatMessages"; + AblyRealtime ablyClient1 = null; + AblyRealtime ablyClient2 = null; + try { + ClientOptions opts1 = createOptions(testVars.keys[7].keyStr); + opts1.clientId = "clientId1"; + ablyClient1 = new AblyRealtime(opts1); + + ClientOptions opts2 = createOptions(testVars.keys[7].keyStr); + opts2.clientId = "clientId2"; + ablyClient2 = new AblyRealtime(opts2); + + ChatRoom room = new ChatRoom(roomId, ablyClient1); + + // Create a channel and attach with client1 + final Channel channel1 = ablyClient1.channels.get(channelName); + channel1.attach(); + (new Helpers.ChannelWaiter(channel1)).waitFor(ChannelState.attached); + + // Subscribe to messages with client2 + final Channel channel2 = ablyClient2.channels.get(channelName); + channel2.attach(); + (new Helpers.ChannelWaiter(channel2)).waitFor(ChannelState.attached); + + List receivedMsg = new ArrayList<>(); + channel2.subscribe(receivedMsg::add); + + // Send message to room + ChatRoom.SendMessageParams sendParams = new ChatRoom.SendMessageParams(); + sendParams.text = "hello there"; + + JsonObject sendMessageResult = (JsonObject) room.sendMessage(sendParams); + String originalSerial = sendMessageResult.get("serial").getAsString(); + + // Wait for the message to be received + Exception err = new Helpers.ConditionalWaiter().wait(() -> !receivedMsg.isEmpty(), 10_000); + Assert.assertNull(err); + + // Delete the message + ChatRoom.DeleteMessageParams deleteParams = new ChatRoom.DeleteMessageParams(); + deleteParams.description = "message deleted by clientId1"; + + room.deleteMessage(originalSerial, deleteParams); + + // Wait for the deleted message to be received + err = new Helpers.ConditionalWaiter().wait(() -> receivedMsg.size() == 2, 10_000); + Assert.assertNull(err); + + // Attempt to update the deleted message + ChatRoom.UpdateMessageParams updateParams = new ChatRoom.UpdateMessageParams(); + updateParams.message = new ChatRoom.SendMessageParams(); + updateParams.message.text = "updated text"; + room.updateMessage(originalSerial, updateParams); + + // wait for updated message to be received + err = new Helpers.ConditionalWaiter().wait(() -> receivedMsg.size() == 3, 10_000); + Assert.assertNull(err); + + // Attempt to delete the already deleted message + room.deleteMessage(originalSerial, deleteParams); + // wait for delete message received + err = new Helpers.ConditionalWaiter().wait(() -> receivedMsg.size() == 4, 10_000); + Assert.assertNull(err); + + Assert.assertEquals(4, receivedMsg.size()); + for (Message msg : receivedMsg) { + Assert.assertEquals("Serial should match original serial", originalSerial, msg.serial); + } + + } catch (Exception e) { + e.printStackTrace(); + Assert.fail("Unexpected exception instantiating library"); + } finally { + if (ablyClient1 != null) ablyClient1.close(); + if (ablyClient2 != null) ablyClient2.close(); + } + } } From c3264ea581cd19c3a5bf13f75be1565955baaa89 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 22 Jan 2025 18:08:35 +0530 Subject: [PATCH 10/10] [ECO-5193][TM*] Fixed Message.Operation.write method for msgpack, updated ChatRoom public methods --- lib/src/main/java/io/ably/lib/types/Message.java | 16 +++++++++++----- lib/src/test/java/io/ably/lib/chat/ChatRoom.java | 14 ++++++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/lib/src/main/java/io/ably/lib/types/Message.java b/lib/src/main/java/io/ably/lib/types/Message.java index 463a361bb..afdea4bc4 100644 --- a/lib/src/main/java/io/ably/lib/types/Message.java +++ b/lib/src/main/java/io/ably/lib/types/Message.java @@ -98,19 +98,25 @@ public static class Operation { public Map metadata; void write(MessagePacker packer) throws IOException { - packer.packMapHeader(3); - if(clientId != null) { + int fieldCount = 0; + if (clientId != null) fieldCount++; + if (description != null) fieldCount++; + if (metadata != null) fieldCount++; + + packer.packMapHeader(fieldCount); + + if (clientId != null) { packer.packString("clientId"); packer.packString(clientId); } - if(description != null) { + if (description != null) { packer.packString("description"); packer.packString(description); } - if(metadata != null) { + if (metadata != null) { packer.packString("metadata"); packer.packMapHeader(metadata.size()); - for(Map.Entry entry : metadata.entrySet()) { + for (Map.Entry entry : metadata.entrySet()) { packer.packString(entry.getKey()); packer.packString(entry.getValue()); } diff --git a/lib/src/test/java/io/ably/lib/chat/ChatRoom.java b/lib/src/test/java/io/ably/lib/chat/ChatRoom.java index 316c21098..5c784a9c9 100644 --- a/lib/src/test/java/io/ably/lib/chat/ChatRoom.java +++ b/lib/src/test/java/io/ably/lib/chat/ChatRoom.java @@ -7,6 +7,7 @@ import io.ably.lib.http.HttpUtils; import io.ably.lib.rest.AblyRest; import io.ably.lib.types.AblyException; +import io.ably.lib.types.ErrorInfo; import io.ably.lib.types.HttpPaginatedResponse; import io.ably.lib.types.Param; @@ -17,6 +18,7 @@ public class ChatRoom { private final AblyRest ablyRest; private final String roomId; + private final Gson gson = new Gson(); protected ChatRoom(String roomId, AblyRest ablyRest) { this.roomId = roomId; @@ -24,18 +26,18 @@ protected ChatRoom(String roomId, AblyRest ablyRest) { } public JsonElement sendMessage(SendMessageParams params) throws Exception { - return makeAuthorizedRequest("/chat/v2/rooms/" + roomId + "/messages", "POST", new Gson().toJsonTree(params)) - .orElseThrow(() -> new Exception("Failed to send message")); + return makeAuthorizedRequest("/chat/v2/rooms/" + roomId + "/messages", "POST", gson.toJsonTree(params)) + .orElseThrow(() -> AblyException.fromErrorInfo(new ErrorInfo("Failed to send message", 500))); } public JsonElement updateMessage(String serial, UpdateMessageParams params) throws Exception { - return makeAuthorizedRequest("/chat/v2/rooms/" + roomId + "/messages/" + serial, "PUT", new Gson().toJsonTree(params)) - .orElseThrow(() -> new Exception("Failed to update message")); + return makeAuthorizedRequest("/chat/v2/rooms/" + roomId + "/messages/" + serial, "PUT", gson.toJsonTree(params)) + .orElseThrow(() -> AblyException.fromErrorInfo(new ErrorInfo("Failed to update message", 500))); } public JsonElement deleteMessage(String serial, DeleteMessageParams params) throws Exception { - return makeAuthorizedRequest("/chat/v2/rooms/" + roomId + "/messages/" + serial + "/delete", "POST", new Gson().toJsonTree(params)) - .orElseThrow(() -> new Exception("Failed to delete message")); + return makeAuthorizedRequest("/chat/v2/rooms/" + roomId + "/messages/" + serial + "/delete", "POST", gson.toJsonTree(params)) + .orElseThrow(() -> AblyException.fromErrorInfo(new ErrorInfo("Failed to delete message", 500))); } public static class SendMessageParams {