From 7baa0e32890a7308560356dacbc9bfb7124eef37 Mon Sep 17 00:00:00 2001 From: Rafael Marinho Date: Thu, 15 Jan 2026 14:43:25 +0100 Subject: [PATCH 01/11] [CHA-1427] support campaigns --- .../getstream/chat/java/models/Campaign.java | 497 ++++++++++++++++++ .../chat/java/services/CampaignService.java | 44 ++ .../io/getstream/chat/java/CampaignTest.java | 202 +++++++ 3 files changed, 743 insertions(+) create mode 100644 src/main/java/io/getstream/chat/java/models/Campaign.java create mode 100644 src/main/java/io/getstream/chat/java/services/CampaignService.java create mode 100644 src/test/java/io/getstream/chat/java/CampaignTest.java diff --git a/src/main/java/io/getstream/chat/java/models/Campaign.java b/src/main/java/io/getstream/chat/java/models/Campaign.java new file mode 100644 index 00000000..00c60d87 --- /dev/null +++ b/src/main/java/io/getstream/chat/java/models/Campaign.java @@ -0,0 +1,497 @@ +package io.getstream.chat.java.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.getstream.chat.java.models.Campaign.CampaignCreateRequestData.CampaignCreateRequest; +import io.getstream.chat.java.models.Campaign.CampaignQueryRequestData.CampaignQueryRequest; +import io.getstream.chat.java.models.Campaign.CampaignStartRequestData.CampaignStartRequest; +import io.getstream.chat.java.models.Campaign.CampaignUpdateRequestData.CampaignUpdateRequest; +import io.getstream.chat.java.models.framework.StreamRequest; +import io.getstream.chat.java.models.framework.StreamResponseObject; +import io.getstream.chat.java.services.CampaignService; +import io.getstream.chat.java.services.framework.Client; +import java.util.Date; +import java.util.List; +import java.util.Map; +import lombok.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import retrofit2.Call; + +@Data +@NoArgsConstructor +public class Campaign { + @NotNull + @JsonProperty("id") + private String id; + + @Nullable + @JsonProperty("name") + private String name; + + @Nullable + @JsonProperty("description") + private String description; + + @NotNull + @JsonProperty("sender_id") + private String senderId; + + @Nullable + @JsonProperty("status") + private String status; + + @Nullable + @JsonProperty("segment_ids") + private List segmentIds; + + @Nullable + @JsonProperty("user_ids") + private List userIds; + + @Nullable + @JsonProperty("message_template") + private MessageTemplate messageTemplate; + + @Nullable + @JsonProperty("channel_template") + private ChannelTemplate channelTemplate; + + @Nullable + @JsonProperty("create_channels") + private Boolean createChannels; + + @Nullable + @JsonProperty("skip_push") + private Boolean skipPush; + + @Nullable + @JsonProperty("skip_webhook") + private Boolean skipWebhook; + + @Nullable + @JsonProperty("scheduled_for") + private Date scheduledFor; + + @Nullable + @JsonProperty("stop_at") + private Date stopAt; + + @NotNull + @JsonProperty("created_at") + private Date createdAt; + + @NotNull + @JsonProperty("updated_at") + private Date updatedAt; + + @Data + @NoArgsConstructor + public static class MessageTemplate { + @Nullable + @JsonProperty("text") + private String text; + + @Nullable + @JsonProperty("attachments") + private List> attachments; + + @Nullable + @JsonProperty("custom") + private Map custom; + } + + @Data + @NoArgsConstructor + public static class MemberTemplate { + @NotNull + @JsonProperty("user_id") + private String userId; + + @Nullable + @JsonProperty("channel_role") + private String channelRole; + + @Nullable + @JsonProperty("custom") + private Map custom; + } + + @Data + @NoArgsConstructor + public static class ChannelTemplate { + @NotNull + @JsonProperty("type") + private String type; + + @Nullable + @JsonProperty("id") + private String id; + + @Nullable + @JsonProperty("members") + private List members; + + @Nullable + @JsonProperty("members_template") + private List membersTemplate; + + @Nullable + @JsonProperty("custom") + private Map custom; + } + + @Builder( + builderClassName = "CampaignCreateRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + @Getter + @EqualsAndHashCode(callSuper = false) + public static class CampaignCreateRequestData { + @Nullable + @JsonProperty("id") + private String id; + + @Nullable + @JsonProperty("name") + private String name; + + @Nullable + @JsonProperty("description") + private String description; + + @Nullable + @JsonProperty("sender_id") + private String senderId; + + @Nullable + @JsonProperty("segment_ids") + private List segmentIds; + + @Nullable + @JsonProperty("user_ids") + private List userIds; + + @Nullable + @JsonProperty("message_template") + private MessageTemplate messageTemplate; + + @Nullable + @JsonProperty("channel_template") + private ChannelTemplate channelTemplate; + + @Nullable + @JsonProperty("create_channels") + private Boolean createChannels; + + @Nullable + @JsonProperty("skip_push") + private Boolean skipPush; + + @Nullable + @JsonProperty("skip_webhook") + private Boolean skipWebhook; + + public static class CampaignCreateRequest extends StreamRequest { + @Override + protected Call generateCall(Client client) { + return client.create(CampaignService.class).create(this.internalBuild()); + } + } + } + + @Getter + @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor + public static class CampaignGetRequest extends StreamRequest { + @NotNull private String id; + + @Override + protected Call generateCall(Client client) { + return client.create(CampaignService.class).get(id); + } + } + + @Builder( + builderClassName = "CampaignUpdateRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + @Getter + @EqualsAndHashCode(callSuper = false) + public static class CampaignUpdateRequestData { + @Nullable + @JsonProperty("name") + private String name; + + @Nullable + @JsonProperty("description") + private String description; + + @Nullable + @JsonProperty("sender_id") + private String senderId; + + @Nullable + @JsonProperty("segment_ids") + private List segmentIds; + + @Nullable + @JsonProperty("user_ids") + private List userIds; + + @Nullable + @JsonProperty("message_template") + private MessageTemplate messageTemplate; + + @Nullable + @JsonProperty("channel_template") + private ChannelTemplate channelTemplate; + + @Nullable + @JsonProperty("create_channels") + private Boolean createChannels; + + @Nullable + @JsonProperty("skip_push") + private Boolean skipPush; + + @Nullable + @JsonProperty("skip_webhook") + private Boolean skipWebhook; + + public static class CampaignUpdateRequest extends StreamRequest { + @NotNull private String id; + + private CampaignUpdateRequest(@NotNull String id) { + this.id = id; + } + + @Override + protected Call generateCall(Client client) { + return client.create(CampaignService.class).update(id, this.internalBuild()); + } + } + } + + @Getter + @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor + public static class CampaignDeleteRequest extends StreamRequest { + @NotNull private String id; + + @Override + protected Call generateCall(Client client) { + return client.create(CampaignService.class).delete(id); + } + } + + @Builder( + builderClassName = "CampaignStartRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + @Getter + @EqualsAndHashCode(callSuper = false) + public static class CampaignStartRequestData { + @Nullable + @JsonProperty("scheduled_for") + private Date scheduledFor; + + @Nullable + @JsonProperty("stop_at") + private Date stopAt; + + public static class CampaignStartRequest extends StreamRequest { + @NotNull private String id; + + private CampaignStartRequest(@NotNull String id) { + this.id = id; + } + + @Override + protected Call generateCall(Client client) { + return client.create(CampaignService.class).start(id, this.internalBuild()); + } + } + } + + @Getter + @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor + public static class CampaignStopRequest extends StreamRequest { + @NotNull private String id; + + @Override + protected Call generateCall(Client client) { + return client.create(CampaignService.class).stop(id); + } + } + + @Builder( + builderClassName = "CampaignQueryRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + @Getter + @EqualsAndHashCode(callSuper = false) + public static class CampaignQueryRequestData { + @Nullable + @JsonProperty("filter") + private Map filter; + + @Singular("sort") + @Nullable + @JsonProperty("sort") + private List sorts; + + @Nullable + @JsonProperty("limit") + private Integer limit; + + @Nullable + @JsonProperty("offset") + private Integer offset; + + public static class CampaignQueryRequest extends StreamRequest { + @Override + protected Call generateCall(Client client) { + return client.create(CampaignService.class).query(this.internalBuild()); + } + } + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class CampaignCreateResponse extends StreamResponseObject { + @NotNull + @JsonProperty("campaign") + private Campaign campaign; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class CampaignGetResponse extends StreamResponseObject { + @NotNull + @JsonProperty("campaign") + private Campaign campaign; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class CampaignUpdateResponse extends StreamResponseObject { + @NotNull + @JsonProperty("campaign") + private Campaign campaign; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class CampaignDeleteResponse extends StreamResponseObject { + @NotNull + @JsonProperty("campaign") + private Campaign campaign; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class CampaignStartResponse extends StreamResponseObject { + @NotNull + @JsonProperty("campaign") + private Campaign campaign; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class CampaignStopResponse extends StreamResponseObject { + @NotNull + @JsonProperty("campaign") + private Campaign campaign; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class CampaignQueryResponse extends StreamResponseObject { + @NotNull + @JsonProperty("campaigns") + private List campaigns; + } + + /** + * Creates a create request + * + * @return the created request + */ + @NotNull + public static CampaignCreateRequest create() { + return new CampaignCreateRequest(); + } + + /** + * Creates a get request + * + * @param id the campaign id + * @return the created request + */ + @NotNull + public static CampaignGetRequest get(@NotNull String id) { + return new CampaignGetRequest(id); + } + + /** + * Creates an update request + * + * @param id the campaign id + * @return the created request + */ + @NotNull + public static CampaignUpdateRequest update(@NotNull String id) { + return new CampaignUpdateRequest(id); + } + + /** + * Creates a delete request + * + * @param id the campaign id + * @return the created request + */ + @NotNull + public static CampaignDeleteRequest delete(@NotNull String id) { + return new CampaignDeleteRequest(id); + } + + /** + * Creates a start request + * + * @param id the campaign id + * @return the created request + */ + @NotNull + public static CampaignStartRequest start(@NotNull String id) { + return new CampaignStartRequest(id); + } + + /** + * Creates a stop request + * + * @param id the campaign id + * @return the created request + */ + @NotNull + public static CampaignStopRequest stop(@NotNull String id) { + return new CampaignStopRequest(id); + } + + /** + * Creates a query request + * + * @return the created request + */ + @NotNull + public static CampaignQueryRequest query() { + return new CampaignQueryRequest(); + } +} diff --git a/src/main/java/io/getstream/chat/java/services/CampaignService.java b/src/main/java/io/getstream/chat/java/services/CampaignService.java new file mode 100644 index 00000000..24ebd613 --- /dev/null +++ b/src/main/java/io/getstream/chat/java/services/CampaignService.java @@ -0,0 +1,44 @@ +package io.getstream.chat.java.services; + +import io.getstream.chat.java.models.Campaign.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import retrofit2.Call; +import retrofit2.http.*; + +public interface CampaignService { + + @POST("campaigns") + @NotNull + Call create( + @NotNull @Body CampaignCreateRequestData campaignCreateRequestData); + + @GET("campaigns/{id}") + @NotNull + Call get(@NotNull @Path("id") String id); + + @PUT("campaigns/{id}") + @NotNull + Call update( + @NotNull @Path("id") String id, + @NotNull @Body CampaignUpdateRequestData campaignUpdateRequestData); + + @DELETE("campaigns/{id}") + @NotNull + Call delete(@NotNull @Path("id") String id); + + @POST("campaigns/{id}/start") + @NotNull + Call start( + @NotNull @Path("id") String id, + @Nullable @Body CampaignStartRequestData campaignStartRequestData); + + @POST("campaigns/{id}/stop") + @NotNull + Call stop(@NotNull @Path("id") String id); + + @POST("campaigns/query") + @NotNull + Call query( + @NotNull @Body CampaignQueryRequestData campaignQueryRequestData); +} diff --git a/src/test/java/io/getstream/chat/java/CampaignTest.java b/src/test/java/io/getstream/chat/java/CampaignTest.java new file mode 100644 index 00000000..e5b855e1 --- /dev/null +++ b/src/test/java/io/getstream/chat/java/CampaignTest.java @@ -0,0 +1,202 @@ +package io.getstream.chat.java; + +import io.getstream.chat.java.models.Campaign; +import io.getstream.chat.java.models.Campaign.MessageTemplate; +import io.getstream.chat.java.models.FilterCondition; +import io.getstream.chat.java.models.Sort; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CampaignTest extends BasicTest { + + @DisplayName("Can create campaign") + @Test + void whenCreatingCampaign_thenCorrectName() { + String campaignName = "test campaign"; + MessageTemplate messageTemplate = new MessageTemplate(); + messageTemplate.setText("Hello"); + + Campaign campaign = + Assertions.assertDoesNotThrow( + () -> + Campaign.create() + .senderId(testUserRequestObject.getId()) + .userIds(List.of(testUsersRequestObjects.get(1).getId())) + .messageTemplate(messageTemplate) + .name(campaignName) + .request()) + .getCampaign(); + Assertions.assertEquals(campaignName, campaign.getName()); + Assertions.assertNotNull(campaign.getId()); + + // Cleanup + Assertions.assertDoesNotThrow(() -> Campaign.delete(campaign.getId()).request()); + } + + @DisplayName("Can perform campaign CRUD operations") + @Test + void whenPerformingCampaignCRUD_thenCorrectOperations() { + MessageTemplate messageTemplate = new MessageTemplate(); + messageTemplate.setText("Hello"); + + // Create + String originalName = "original name"; + Campaign created = + Assertions.assertDoesNotThrow( + () -> + Campaign.create() + .senderId(testUserRequestObject.getId()) + .userIds(List.of(testUsersRequestObjects.get(1).getId())) + .messageTemplate(messageTemplate) + .name(originalName) + .request()) + .getCampaign(); + Assertions.assertEquals(originalName, created.getName()); + String campaignId = created.getId(); + + pause(); + + // Read + Campaign retrieved = + Assertions.assertDoesNotThrow(() -> Campaign.get(campaignId).request()).getCampaign(); + Assertions.assertEquals(campaignId, retrieved.getId()); + Assertions.assertEquals(originalName, retrieved.getName()); + + // Update + String updatedName = "updated name"; + Campaign updated = + Assertions.assertDoesNotThrow( + () -> + Campaign.update(campaignId) + .name(updatedName) + .messageTemplate(messageTemplate) + .senderId(testUserRequestObject.getId()) + .userIds(List.of(testUsersRequestObjects.get(1).getId())) + .request()) + .getCampaign(); + Assertions.assertEquals(updatedName, updated.getName()); + + pause(); + + // Delete + Assertions.assertDoesNotThrow(() -> Campaign.delete(campaignId).request()); + } + + @DisplayName("Can start and stop campaign") + @Test + void whenStartingAndStoppingCampaign_thenCorrectOperations() { + MessageTemplate messageTemplate = new MessageTemplate(); + messageTemplate.setText("Hello"); + + Campaign created = + Assertions.assertDoesNotThrow( + () -> + Campaign.create() + .senderId(testUserRequestObject.getId()) + .userIds(List.of(testUsersRequestObjects.get(1).getId())) + .messageTemplate(messageTemplate) + .name("test campaign") + .request()) + .getCampaign(); + String campaignId = created.getId(); + + pause(); + + // Start with scheduling + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.HOUR, 1); + Date scheduledFor = cal.getTime(); + cal.add(Calendar.HOUR, 1); + Date stopAt = cal.getTime(); + + Campaign started = + Assertions.assertDoesNotThrow( + () -> + Campaign.start(campaignId) + .scheduledFor(scheduledFor) + .stopAt(stopAt) + .request()) + .getCampaign(); + Assertions.assertNotNull(started.getScheduledFor()); + + pause(); + + // Stop + Campaign stopped = + Assertions.assertDoesNotThrow(() -> Campaign.stop(campaignId).request()).getCampaign(); + Assertions.assertNotNull(stopped); + + // Cleanup + Assertions.assertDoesNotThrow(() -> Campaign.delete(campaignId).request()); + } + + @DisplayName("Can query campaigns") + @Test + void whenQueryingCampaigns_thenCorrectResults() { + MessageTemplate messageTemplate = new MessageTemplate(); + messageTemplate.setText("Hello"); + + Campaign created = + Assertions.assertDoesNotThrow( + () -> + Campaign.create() + .senderId(testUserRequestObject.getId()) + .userIds(List.of(testUsersRequestObjects.get(1).getId())) + .messageTemplate(messageTemplate) + .name("query test campaign") + .request()) + .getCampaign(); + String campaignId = created.getId(); + + pause(); + + // Query by ID + List campaigns = + Assertions.assertDoesNotThrow( + () -> + Campaign.query() + .filter(FilterCondition.eq("id", campaignId)) + .sorts( + List.of( + Sort.builder() + .field("created_at") + .direction(Sort.Direction.DESC) + .build())) + .limit(10) + .request()) + .getCampaigns(); + Assertions.assertTrue( + campaigns.stream().anyMatch(c -> c.getId().equals(campaignId)), + "Created campaign should be found in query results"); + + // Cleanup + Assertions.assertDoesNotThrow(() -> Campaign.delete(campaignId).request()); + } + + @DisplayName("Can create campaign without ID") + @Test + void whenCreatingCampaignWithoutId_thenIdIsGenerated() { + MessageTemplate messageTemplate = new MessageTemplate(); + messageTemplate.setText("Hello"); + + Campaign campaign = + Assertions.assertDoesNotThrow( + () -> + Campaign.create() + .senderId(testUserRequestObject.getId()) + .userIds(List.of(testUsersRequestObjects.get(1).getId())) + .messageTemplate(messageTemplate) + .name("auto id campaign") + .request()) + .getCampaign(); + Assertions.assertNotNull(campaign.getId()); + Assertions.assertFalse(campaign.getId().isEmpty()); + + // Cleanup + Assertions.assertDoesNotThrow(() -> Campaign.delete(campaign.getId()).request()); + } +} From ca29a254f4768ad6dd30ff9a9eae44d0720aaa4d Mon Sep 17 00:00:00 2001 From: Rafael Marinho Date: Fri, 16 Jan 2026 10:28:00 +0100 Subject: [PATCH 02/11] fix format --- src/test/java/io/getstream/chat/java/CampaignTest.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test/java/io/getstream/chat/java/CampaignTest.java b/src/test/java/io/getstream/chat/java/CampaignTest.java index e5b855e1..5f455e24 100644 --- a/src/test/java/io/getstream/chat/java/CampaignTest.java +++ b/src/test/java/io/getstream/chat/java/CampaignTest.java @@ -116,10 +116,7 @@ void whenStartingAndStoppingCampaign_thenCorrectOperations() { Campaign started = Assertions.assertDoesNotThrow( () -> - Campaign.start(campaignId) - .scheduledFor(scheduledFor) - .stopAt(stopAt) - .request()) + Campaign.start(campaignId).scheduledFor(scheduledFor).stopAt(stopAt).request()) .getCampaign(); Assertions.assertNotNull(started.getScheduledFor()); From 39d2597260fd5e88b55b24590158b90a540f1b76 Mon Sep 17 00:00:00 2001 From: Rafael Marinho Date: Fri, 16 Jan 2026 10:42:17 +0100 Subject: [PATCH 03/11] fix tests --- .../chat/java/SharedLocationTest.java | 91 ++++++++++++++----- 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/src/test/java/io/getstream/chat/java/SharedLocationTest.java b/src/test/java/io/getstream/chat/java/SharedLocationTest.java index ee21a713..eeed63b6 100644 --- a/src/test/java/io/getstream/chat/java/SharedLocationTest.java +++ b/src/test/java/io/getstream/chat/java/SharedLocationTest.java @@ -36,6 +36,25 @@ static void setupSharedLocations() { .request()); } + /** + * Helper method to create an endAt date that is at least 2 minutes in the future + * to ensure it passes the server validation requirement of being more than 1 minute in the future. + */ + private Date createFutureEndAt() { + // Add 5 minutes to current time to ensure it's well beyond the 1 minute requirement + // This accounts for any network delays or processing time + return new Date(System.currentTimeMillis() + (5 * 60 * 1000)); + } + + /** + * Helper method to format a Date to ISO 8601 string format + */ + private String formatDateToISO(Date date) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return dateFormat.format(date); + } + @DisplayName("Can send message with shared location and verify") @Test void whenSendingMessageWithSharedLocation_thenCanGetThroughUsersLocations() @@ -43,12 +62,18 @@ void whenSendingMessageWithSharedLocation_thenCanGetThroughUsersLocations() // Create a unique device ID for this test String deviceId = "device-" + UUID.randomUUID().toString(); + // Create a future endAt date (at least 2 minutes in the future) + Date endAtDate = createFutureEndAt(); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + String endAtString = formatDateToISO(endAtDate); + // Create shared location request SharedLocationRequest locationRequest = new SharedLocation.SharedLocationRequest(); locationRequest.setCreatedByDeviceId(deviceId); locationRequest.setLatitude(40.7128); locationRequest.setLongitude(-74.0060); - locationRequest.setEndAt("2025-12-31T23:59:59Z"); + locationRequest.setEndAt(endAtString); locationRequest.setUserId(testUserRequestObject.getId()); // Convert request to SharedLocation @@ -56,9 +81,7 @@ void whenSendingMessageWithSharedLocation_thenCanGetThroughUsersLocations() sharedLocation.setCreatedByDeviceId(locationRequest.getCreatedByDeviceId()); sharedLocation.setLatitude(locationRequest.getLatitude()); sharedLocation.setLongitude(locationRequest.getLongitude()); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - sharedLocation.setEndAt(dateFormat.parse(locationRequest.getEndAt())); + sharedLocation.setEndAt(endAtDate); // Send message with shared location MessageRequestObject messageRequest = @@ -82,9 +105,11 @@ void whenSendingMessageWithSharedLocation_thenCanGetThroughUsersLocations() Assertions.assertEquals(40.7128, message.getSharedLocation().getLatitude()); Assertions.assertEquals(-74.0060, message.getSharedLocation().getLongitude()); - // Parse and verify the endAt date - Date expectedEndAt = dateFormat.parse("2025-12-31T23:59:59Z"); - Assertions.assertEquals(expectedEndAt, message.getSharedLocation().getEndAt()); + // Verify the endAt date is set (allowing for small timing differences) + Assertions.assertNotNull(message.getSharedLocation().getEndAt()); + // The endAt should be close to our expected time (within a reasonable range) + long timeDiff = Math.abs(message.getSharedLocation().getEndAt().getTime() - endAtDate.getTime()); + Assertions.assertTrue(timeDiff < 60000, "EndAt time should be within 1 minute of expected time"); } @DisplayName("Can create live location, update it and verify the update") @@ -93,12 +118,18 @@ void whenUpdatingLiveLocation_thenCanGetUpdatedLocation() throws StreamException // Create a unique device ID for this test String deviceId = "device-" + UUID.randomUUID().toString(); + // Create a future endAt date (at least 2 minutes in the future) + Date initialEndAtDate = createFutureEndAt(); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + String initialEndAtString = formatDateToISO(initialEndAtDate); + // Create initial shared location request SharedLocationRequest initialLocationRequest = new SharedLocation.SharedLocationRequest(); initialLocationRequest.setCreatedByDeviceId(deviceId); initialLocationRequest.setLatitude(40.7128); initialLocationRequest.setLongitude(-74.0060); - initialLocationRequest.setEndAt("2025-12-31T23:59:59Z"); + initialLocationRequest.setEndAt(initialEndAtString); initialLocationRequest.setUserId(testUserRequestObject.getId()); // Convert request to SharedLocation @@ -106,9 +137,7 @@ void whenUpdatingLiveLocation_thenCanGetUpdatedLocation() throws StreamException initialSharedLocation.setCreatedByDeviceId(initialLocationRequest.getCreatedByDeviceId()); initialSharedLocation.setLatitude(initialLocationRequest.getLatitude()); initialSharedLocation.setLongitude(initialLocationRequest.getLongitude()); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - initialSharedLocation.setEndAt(dateFormat.parse(initialLocationRequest.getEndAt())); + initialSharedLocation.setEndAt(initialEndAtDate); // Send initial message with shared location MessageRequestObject initialMessageRequest = @@ -125,13 +154,16 @@ void whenUpdatingLiveLocation_thenCanGetUpdatedLocation() throws StreamException .request() .getMessage(); - // Create updated location request + // Create updated location request with a new future endAt date + Date updatedEndAtDate = createFutureEndAt(); + String updatedEndAtString = formatDateToISO(updatedEndAtDate); + SharedLocationRequest updatedLocationRequest = new SharedLocation.SharedLocationRequest(); updatedLocationRequest.setMessageId(initialMessage.getId()); updatedLocationRequest.setCreatedByDeviceId(deviceId); updatedLocationRequest.setLatitude(40.7589); // Updated latitude updatedLocationRequest.setLongitude(-73.9851); // Updated longitude - updatedLocationRequest.setEndAt("2025-12-31T23:59:59Z"); + updatedLocationRequest.setEndAt(updatedEndAtString); updatedLocationRequest.setUserId(testUserRequestObject.getId()); // Update the location @@ -162,9 +194,10 @@ void whenUpdatingLiveLocation_thenCanGetUpdatedLocation() throws StreamException Assertions.assertEquals(40.7589, updatedLocation.getLatitude()); Assertions.assertEquals(-73.9851, updatedLocation.getLongitude()); - // Verify the endAt date - Date expectedEndAt = dateFormat.parse("2025-12-31T23:59:59Z"); - Assertions.assertEquals(expectedEndAt, updatedLocation.getEndAt()); + // Verify the endAt date is set (allowing for small timing differences) + Assertions.assertNotNull(updatedLocation.getEndAt()); + long timeDiff = Math.abs(updatedLocation.getEndAt().getTime() - updatedEndAtDate.getTime()); + Assertions.assertTrue(timeDiff < 60000, "EndAt time should be within 1 minute of expected time"); } @DisplayName("Can verify live location in channel") @@ -173,12 +206,18 @@ void whenQueryingChannel_thenShouldHaveLiveLocation() throws StreamException, Pa // Create a unique device ID for this test String deviceId = "device-" + UUID.randomUUID().toString(); + // Create a future endAt date (at least 2 minutes in the future) + Date endAtDate = createFutureEndAt(); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + String endAtString = formatDateToISO(endAtDate); + // Create shared location request SharedLocationRequest locationRequest = new SharedLocation.SharedLocationRequest(); locationRequest.setCreatedByDeviceId(deviceId); locationRequest.setLatitude(40.7128); locationRequest.setLongitude(-74.0060); - locationRequest.setEndAt("2025-12-31T23:59:59Z"); + locationRequest.setEndAt(endAtString); locationRequest.setUserId(testUserRequestObject.getId()); // Convert request to SharedLocation @@ -186,9 +225,7 @@ void whenQueryingChannel_thenShouldHaveLiveLocation() throws StreamException, Pa sharedLocation.setCreatedByDeviceId(locationRequest.getCreatedByDeviceId()); sharedLocation.setLatitude(locationRequest.getLatitude()); sharedLocation.setLongitude(locationRequest.getLongitude()); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - sharedLocation.setEndAt(dateFormat.parse(locationRequest.getEndAt())); + sharedLocation.setEndAt(endAtDate); // Send message with shared location MessageRequestObject messageRequest = @@ -212,9 +249,10 @@ void whenQueryingChannel_thenShouldHaveLiveLocation() throws StreamException, Pa Assertions.assertEquals(40.7128, message.getSharedLocation().getLatitude()); Assertions.assertEquals(-74.0060, message.getSharedLocation().getLongitude()); - // Parse and verify the endAt date - Date expectedEndAt = dateFormat.parse("2025-12-31T23:59:59Z"); - Assertions.assertEquals(expectedEndAt, message.getSharedLocation().getEndAt()); + // Verify the endAt date is set (allowing for small timing differences) + Assertions.assertNotNull(message.getSharedLocation().getEndAt()); + long timeDiff = Math.abs(message.getSharedLocation().getEndAt().getTime() - endAtDate.getTime()); + Assertions.assertTrue(timeDiff < 60000, "EndAt time should be within 1 minute of expected time"); // Query the channel to verify it has the live location ChannelGetResponse response = @@ -238,6 +276,9 @@ void whenQueryingChannel_thenShouldHaveLiveLocation() throws StreamException, Pa Assertions.assertEquals(deviceId, channelLocation.getCreatedByDeviceId()); Assertions.assertEquals(40.7128, channelLocation.getLatitude()); Assertions.assertEquals(-74.0060, channelLocation.getLongitude()); - Assertions.assertEquals(expectedEndAt, channelLocation.getEndAt()); + Assertions.assertNotNull(channelLocation.getEndAt()); + // Allow for small timing differences in the endAt comparison + long channelTimeDiff = Math.abs(channelLocation.getEndAt().getTime() - endAtDate.getTime()); + Assertions.assertTrue(channelTimeDiff < 60000, "EndAt time should be within 1 minute of expected time"); } -} +} \ No newline at end of file From ab1bbb45028d92c71cd01de2e46759a770de3a46 Mon Sep 17 00:00:00 2001 From: Rafael Marinho Date: Fri, 16 Jan 2026 10:44:28 +0100 Subject: [PATCH 04/11] fix tests --- .../chat/java/SharedLocationTest.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/test/java/io/getstream/chat/java/SharedLocationTest.java b/src/test/java/io/getstream/chat/java/SharedLocationTest.java index eeed63b6..b25fbba8 100644 --- a/src/test/java/io/getstream/chat/java/SharedLocationTest.java +++ b/src/test/java/io/getstream/chat/java/SharedLocationTest.java @@ -37,8 +37,8 @@ static void setupSharedLocations() { } /** - * Helper method to create an endAt date that is at least 2 minutes in the future - * to ensure it passes the server validation requirement of being more than 1 minute in the future. + * Helper method to create an endAt date that is at least 2 minutes in the future to ensure it + * passes the server validation requirement of being more than 1 minute in the future. */ private Date createFutureEndAt() { // Add 5 minutes to current time to ensure it's well beyond the 1 minute requirement @@ -46,9 +46,7 @@ private Date createFutureEndAt() { return new Date(System.currentTimeMillis() + (5 * 60 * 1000)); } - /** - * Helper method to format a Date to ISO 8601 string format - */ + /** Helper method to format a Date to ISO 8601 string format */ private String formatDateToISO(Date date) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -108,8 +106,10 @@ void whenSendingMessageWithSharedLocation_thenCanGetThroughUsersLocations() // Verify the endAt date is set (allowing for small timing differences) Assertions.assertNotNull(message.getSharedLocation().getEndAt()); // The endAt should be close to our expected time (within a reasonable range) - long timeDiff = Math.abs(message.getSharedLocation().getEndAt().getTime() - endAtDate.getTime()); - Assertions.assertTrue(timeDiff < 60000, "EndAt time should be within 1 minute of expected time"); + long timeDiff = + Math.abs(message.getSharedLocation().getEndAt().getTime() - endAtDate.getTime()); + Assertions.assertTrue( + timeDiff < 60000, "EndAt time should be within 1 minute of expected time"); } @DisplayName("Can create live location, update it and verify the update") @@ -197,7 +197,8 @@ void whenUpdatingLiveLocation_thenCanGetUpdatedLocation() throws StreamException // Verify the endAt date is set (allowing for small timing differences) Assertions.assertNotNull(updatedLocation.getEndAt()); long timeDiff = Math.abs(updatedLocation.getEndAt().getTime() - updatedEndAtDate.getTime()); - Assertions.assertTrue(timeDiff < 60000, "EndAt time should be within 1 minute of expected time"); + Assertions.assertTrue( + timeDiff < 60000, "EndAt time should be within 1 minute of expected time"); } @DisplayName("Can verify live location in channel") @@ -251,8 +252,10 @@ void whenQueryingChannel_thenShouldHaveLiveLocation() throws StreamException, Pa // Verify the endAt date is set (allowing for small timing differences) Assertions.assertNotNull(message.getSharedLocation().getEndAt()); - long timeDiff = Math.abs(message.getSharedLocation().getEndAt().getTime() - endAtDate.getTime()); - Assertions.assertTrue(timeDiff < 60000, "EndAt time should be within 1 minute of expected time"); + long timeDiff = + Math.abs(message.getSharedLocation().getEndAt().getTime() - endAtDate.getTime()); + Assertions.assertTrue( + timeDiff < 60000, "EndAt time should be within 1 minute of expected time"); // Query the channel to verify it has the live location ChannelGetResponse response = @@ -279,6 +282,7 @@ void whenQueryingChannel_thenShouldHaveLiveLocation() throws StreamException, Pa Assertions.assertNotNull(channelLocation.getEndAt()); // Allow for small timing differences in the endAt comparison long channelTimeDiff = Math.abs(channelLocation.getEndAt().getTime() - endAtDate.getTime()); - Assertions.assertTrue(channelTimeDiff < 60000, "EndAt time should be within 1 minute of expected time"); + Assertions.assertTrue( + channelTimeDiff < 60000, "EndAt time should be within 1 minute of expected time"); } -} \ No newline at end of file +} From 1b7097cbc8839be4a35f43b2d5f82f060e316574 Mon Sep 17 00:00:00 2001 From: Rafael Marinho Date: Fri, 16 Jan 2026 11:22:53 +0100 Subject: [PATCH 05/11] [CHA-1427] support polls --- .../io/getstream/chat/java/models/Poll.java | 889 ++++++++++++++++++ .../chat/java/services/PollService.java | 80 ++ .../java/io/getstream/chat/java/PollTest.java | 270 ++++++ 3 files changed, 1239 insertions(+) create mode 100644 src/main/java/io/getstream/chat/java/models/Poll.java create mode 100644 src/main/java/io/getstream/chat/java/services/PollService.java create mode 100644 src/test/java/io/getstream/chat/java/PollTest.java diff --git a/src/main/java/io/getstream/chat/java/models/Poll.java b/src/main/java/io/getstream/chat/java/models/Poll.java new file mode 100644 index 00000000..6b1b0d61 --- /dev/null +++ b/src/main/java/io/getstream/chat/java/models/Poll.java @@ -0,0 +1,889 @@ +package io.getstream.chat.java.models; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.getstream.chat.java.models.Poll.PollCreateRequestData.PollCreateRequest; +import io.getstream.chat.java.models.Poll.PollOptionCreateRequestData.PollOptionCreateRequest; +import io.getstream.chat.java.models.Poll.PollOptionUpdateRequestData.PollOptionUpdateRequest; +import io.getstream.chat.java.models.Poll.PollQueryRequestData.PollQueryRequest; +import io.getstream.chat.java.models.Poll.PollUpdatePartialRequestData.PollUpdatePartialRequest; +import io.getstream.chat.java.models.Poll.PollUpdateRequestData.PollUpdateRequest; +import io.getstream.chat.java.models.Poll.PollVoteCastRequestData.PollVoteCastRequest; +import io.getstream.chat.java.models.Poll.PollVoteQueryRequestData.PollVoteQueryRequest; +import io.getstream.chat.java.models.framework.StreamRequest; +import io.getstream.chat.java.models.framework.StreamResponseObject; +import io.getstream.chat.java.services.PollService; +import io.getstream.chat.java.services.framework.Client; +import java.util.Date; +import java.util.List; +import java.util.Map; +import lombok.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import retrofit2.Call; + +@Data +@NoArgsConstructor +public class Poll { + @NotNull + @JsonProperty("id") + private String id; + + @NotNull + @JsonProperty("name") + private String name; + + @Nullable + @JsonProperty("description") + private String description; + + @Nullable + @JsonProperty("voting_visibility") + private VotingVisibility votingVisibility; + + @NotNull + @JsonProperty("enforce_unique_vote") + private Boolean enforceUniqueVote; + + @Nullable + @JsonProperty("max_votes_allowed") + private Integer maxVotesAllowed; + + @NotNull + @JsonProperty("allow_user_suggested_options") + private Boolean allowUserSuggestedOptions; + + @NotNull + @JsonProperty("allow_answers") + private Boolean allowAnswers; + + @NotNull + @JsonProperty("is_closed") + private Boolean isClosed; + + @Nullable + @JsonProperty("options") + private List options; + + @Nullable + @JsonProperty("vote_count") + private Integer voteCount; + + @Nullable + @JsonProperty("vote_counts_by_option") + private Map voteCountsByOption; + + @Nullable + @JsonProperty("answers_count") + private Integer answersCount; + + @Nullable + @JsonProperty("latest_votes_by_option") + private Map> latestVotesByOption; + + @Nullable + @JsonProperty("latest_answers") + private List latestAnswers; + + @Nullable + @JsonProperty("own_votes") + private List ownVotes; + + @NotNull + @JsonProperty("created_by_id") + private String createdById; + + @Nullable + @JsonProperty("created_by") + private User createdBy; + + @NotNull + @JsonProperty("created_at") + private Date createdAt; + + @NotNull + @JsonProperty("updated_at") + private Date updatedAt; + + @Nullable + @JsonProperty("custom") + private Map custom; + + public enum VotingVisibility { + @JsonProperty("public") + PUBLIC, + @JsonProperty("anonymous") + ANONYMOUS, + @JsonEnumDefaultValue + UNKNOWN + } + + @Data + @NoArgsConstructor + public static class PollOption { + @NotNull + @JsonProperty("id") + private String id; + + @Nullable + @JsonProperty("text") + private String text; + + @Nullable + @JsonProperty("custom") + private Map custom; + } + + @Data + @NoArgsConstructor + public static class PollVote { + @NotNull + @JsonProperty("poll_id") + private String pollId; + + @NotNull + @JsonProperty("id") + private String id; + + @Nullable + @JsonProperty("option_id") + private String optionId; + + @NotNull + @JsonProperty("is_answer") + private Boolean isAnswer; + + @Nullable + @JsonProperty("answer_text") + private String answerText; + + @Nullable + @JsonProperty("user_id") + private String userId; + + @Nullable + @JsonProperty("user") + private User user; + + @NotNull + @JsonProperty("created_at") + private Date createdAt; + + @NotNull + @JsonProperty("updated_at") + private Date updatedAt; + } + + @Builder( + builderClassName = "PollCreateRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + @Getter + @EqualsAndHashCode(callSuper = false) + public static class PollCreateRequestData { + @Nullable + @JsonProperty("id") + private String id; + + @NotNull + @JsonProperty("name") + private String name; + + @Nullable + @JsonProperty("description") + private String description; + + @Singular + @Nullable + @JsonProperty("options") + private List options; + + @Nullable + @JsonProperty("voting_visibility") + private VotingVisibility votingVisibility; + + @Nullable + @JsonProperty("enforce_unique_vote") + private Boolean enforceUniqueVote; + + @Nullable + @JsonProperty("max_votes_allowed") + private Integer maxVotesAllowed; + + @Nullable + @JsonProperty("allow_user_suggested_options") + private Boolean allowUserSuggestedOptions; + + @Nullable + @JsonProperty("allow_answers") + private Boolean allowAnswers; + + @Nullable + @JsonProperty("is_closed") + private Boolean isClosed; + + @Nullable + @JsonProperty("custom") + private Map custom; + + @Data + @NoArgsConstructor + public static class PollOptionInput { + @Nullable + @JsonProperty("id") + private String id; + + @Nullable + @JsonProperty("text") + private String text; + + @Nullable + @JsonProperty("custom") + private Map custom; + } + + public static class PollCreateRequest extends StreamRequest { + @Override + protected Call generateCall(Client client) { + return client.create(PollService.class).create(this.internalBuild()); + } + } + } + + @Getter + @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor + public static class PollGetRequest extends StreamRequest { + @NotNull private String pollId; + + @Override + protected Call generateCall(Client client) { + return client.create(PollService.class).get(pollId); + } + } + + @Builder( + builderClassName = "PollUpdateRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + @Getter + @Setter + @EqualsAndHashCode(callSuper = false) + public static class PollUpdateRequestData { + @NotNull + @JsonProperty("id") + private String id; + + @NotNull + @JsonProperty("name") + private String name; + + @Nullable + @JsonProperty("description") + private String description; + + @Singular + @Nullable + @JsonProperty("options") + private List options; + + @Nullable + @JsonProperty("voting_visibility") + private VotingVisibility votingVisibility; + + @Nullable + @JsonProperty("enforce_unique_vote") + private Boolean enforceUniqueVote; + + @Nullable + @JsonProperty("max_votes_allowed") + private Integer maxVotesAllowed; + + @Nullable + @JsonProperty("allow_user_suggested_options") + private Boolean allowUserSuggestedOptions; + + @Nullable + @JsonProperty("allow_answers") + private Boolean allowAnswers; + + @Nullable + @JsonProperty("is_closed") + private Boolean isClosed; + + @Nullable + @JsonProperty("custom") + private Map custom; + + @Data + @NoArgsConstructor + public static class PollOptionRequest { + @NotNull + @JsonProperty("id") + private String id; + + @Nullable + @JsonProperty("text") + private String text; + + @Nullable + @JsonProperty("custom") + private Map custom; + } + + public static class PollUpdateRequest extends StreamRequest { + @NotNull private String pollId; + + private PollUpdateRequest(@NotNull String pollId) { + this.pollId = pollId; + } + + @Override + protected Call generateCall(Client client) { + PollUpdateRequestData data = this.internalBuild(); + data.setId(pollId); + return client.create(PollService.class).update(data); + } + } + } + + @Builder( + builderClassName = "PollUpdatePartialRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + @Getter + @EqualsAndHashCode(callSuper = false) + public static class PollUpdatePartialRequestData { + @Nullable + @JsonProperty("set") + private Map set; + + @Singular("unset") + @Nullable + @JsonProperty("unset") + private List unset; + + public static class PollUpdatePartialRequest extends StreamRequest { + @NotNull private String pollId; + + private PollUpdatePartialRequest(@NotNull String pollId) { + this.pollId = pollId; + } + + @Override + protected Call generateCall(Client client) { + return client.create(PollService.class).updatePartial(pollId, this.internalBuild()); + } + } + } + + @Getter + @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor + public static class PollDeleteRequest extends StreamRequest { + @NotNull private String pollId; + + @Override + protected Call generateCall(Client client) { + return client.create(PollService.class).delete(pollId); + } + } + + @Builder( + builderClassName = "PollQueryRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + @Getter + @EqualsAndHashCode(callSuper = false) + public static class PollQueryRequestData { + @Nullable + @JsonProperty("filter") + private Map filter; + + @Singular("sort") + @Nullable + @JsonProperty("sort") + private List sorts; + + @Nullable + @JsonProperty("limit") + private Integer limit; + + @Nullable + @JsonProperty("offset") + private Integer offset; + + @Nullable + @JsonProperty("next") + private String next; + + @Nullable + @JsonProperty("prev") + private String prev; + + public static class PollQueryRequest extends StreamRequest { + @Override + protected Call generateCall(Client client) { + return client.create(PollService.class).query(this.internalBuild()); + } + } + } + + @Builder( + builderClassName = "PollOptionCreateRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + @Getter + @EqualsAndHashCode(callSuper = false) + public static class PollOptionCreateRequestData { + @NotNull + @JsonProperty("text") + private String text; + + @Nullable + @JsonProperty("custom") + private Map custom; + + public static class PollOptionCreateRequest extends StreamRequest { + @NotNull private String pollId; + + private PollOptionCreateRequest(@NotNull String pollId) { + this.pollId = pollId; + } + + @Override + protected Call generateCall(Client client) { + return client.create(PollService.class).createOption(pollId, this.internalBuild()); + } + } + } + + @Getter + @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor + public static class PollOptionGetRequest extends StreamRequest { + @NotNull private String pollId; + @NotNull private String optionId; + + @Override + protected Call generateCall(Client client) { + return client.create(PollService.class).getOption(pollId, optionId); + } + } + + @Builder( + builderClassName = "PollOptionUpdateRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + @Getter + @EqualsAndHashCode(callSuper = false) + public static class PollOptionUpdateRequestData { + @NotNull + @JsonProperty("id") + private String id; + + @NotNull + @JsonProperty("text") + private String text; + + @Nullable + @JsonProperty("custom") + private Map custom; + + public static class PollOptionUpdateRequest extends StreamRequest { + @NotNull private String pollId; + + private PollOptionUpdateRequest(@NotNull String pollId) { + this.pollId = pollId; + } + + @Override + protected Call generateCall(Client client) { + return client.create(PollService.class).updateOption(pollId, this.internalBuild()); + } + } + } + + @Getter + @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor + public static class PollOptionDeleteRequest extends StreamRequest { + @NotNull private String pollId; + @NotNull private String optionId; + + @Override + protected Call generateCall(Client client) { + return client.create(PollService.class).deleteOption(pollId, optionId); + } + } + + @Builder( + builderClassName = "PollVoteQueryRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + @Getter + @EqualsAndHashCode(callSuper = false) + public static class PollVoteQueryRequestData { + @Nullable + @JsonProperty("filter") + private Map filter; + + @Singular("sort") + @Nullable + @JsonProperty("sort") + private List sorts; + + @Nullable + @JsonProperty("limit") + private Integer limit; + + @Nullable + @JsonProperty("offset") + private Integer offset; + + @Nullable + @JsonProperty("next") + private String next; + + @Nullable + @JsonProperty("prev") + private String prev; + + public static class PollVoteQueryRequest extends StreamRequest { + @NotNull private String pollId; + + private PollVoteQueryRequest(@NotNull String pollId) { + this.pollId = pollId; + } + + @Override + protected Call generateCall(Client client) { + return client.create(PollService.class).queryVotes(pollId, this.internalBuild()); + } + } + } + + @Builder( + builderClassName = "PollVoteCastRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + @Getter + @EqualsAndHashCode(callSuper = false) + public static class PollVoteCastRequestData { + @Nullable + @JsonProperty("vote") + private VoteData vote; + + @Data + @NoArgsConstructor + public static class VoteData { + @Nullable + @JsonProperty("option_id") + private String optionId; + + @Nullable + @JsonProperty("answer_text") + private String answerText; + } + + public static class PollVoteCastRequest extends StreamRequest { + @NotNull private String messageId; + @NotNull private String pollId; + + private PollVoteCastRequest(@NotNull String messageId, @NotNull String pollId) { + this.messageId = messageId; + this.pollId = pollId; + } + + @Override + protected Call generateCall(Client client) { + return client.create(PollService.class).castVote(messageId, pollId, this.internalBuild()); + } + } + } + + @Getter + @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor + public static class PollVoteDeleteRequest extends StreamRequest { + @NotNull private String messageId; + @NotNull private String pollId; + @NotNull private String voteId; + + @Override + protected Call generateCall(Client client) { + return client.create(PollService.class).deleteVote(messageId, pollId, voteId); + } + } + + // Response classes + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollCreateResponse extends StreamResponseObject { + @NotNull + @JsonProperty("poll") + private Poll poll; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollGetResponse extends StreamResponseObject { + @NotNull + @JsonProperty("poll") + private Poll poll; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollUpdateResponse extends StreamResponseObject { + @NotNull + @JsonProperty("poll") + private Poll poll; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollUpdatePartialResponse extends StreamResponseObject { + @NotNull + @JsonProperty("poll") + private Poll poll; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollDeleteResponse extends StreamResponseObject { + @NotNull + @JsonProperty("poll") + private Poll poll; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollQueryResponse extends StreamResponseObject { + @NotNull + @JsonProperty("polls") + private List polls; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollOptionCreateResponse extends StreamResponseObject { + @NotNull + @JsonProperty("poll_option") + private PollOption pollOption; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollOptionGetResponse extends StreamResponseObject { + @NotNull + @JsonProperty("poll_option") + private PollOption pollOption; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollOptionUpdateResponse extends StreamResponseObject { + @NotNull + @JsonProperty("poll_option") + private PollOption pollOption; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollOptionDeleteResponse extends StreamResponseObject { + @NotNull + @JsonProperty("poll_option") + private PollOption pollOption; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollVoteQueryResponse extends StreamResponseObject { + @NotNull + @JsonProperty("votes") + private List votes; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollVoteCastResponse extends StreamResponseObject { + @Nullable + @JsonProperty("vote") + private PollVote vote; + + @Nullable + @JsonProperty("poll") + private Poll poll; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class PollVoteDeleteResponse extends StreamResponseObject { + @NotNull + @JsonProperty("poll") + private Poll poll; + } + + // Factory methods + /** + * Creates a create request + * + * @return the created request + */ + @NotNull + public static PollCreateRequest create() { + return new PollCreateRequest(); + } + + /** + * Creates a get request + * + * @param pollId the poll id + * @return the created request + */ + @NotNull + public static PollGetRequest get(@NotNull String pollId) { + return new PollGetRequest(pollId); + } + + /** + * Creates an update request + * + * @param pollId the poll id + * @return the created request + */ + @NotNull + public static PollUpdateRequest update(@NotNull String pollId) { + return new PollUpdateRequest(pollId); + } + + /** + * Creates a partial update request + * + * @param pollId the poll id + * @return the created request + */ + @NotNull + public static PollUpdatePartialRequest updatePartial(@NotNull String pollId) { + return new PollUpdatePartialRequest(pollId); + } + + /** + * Creates a delete request + * + * @param pollId the poll id + * @return the created request + */ + @NotNull + public static PollDeleteRequest delete(@NotNull String pollId) { + return new PollDeleteRequest(pollId); + } + + /** + * Creates a query request + * + * @return the created request + */ + @NotNull + public static PollQueryRequest query() { + return new PollQueryRequest(); + } + + /** + * Creates a create option request + * + * @param pollId the poll id + * @return the created request + */ + @NotNull + public static PollOptionCreateRequest createOption(@NotNull String pollId) { + return new PollOptionCreateRequest(pollId); + } + + /** + * Creates a get option request + * + * @param pollId the poll id + * @param optionId the option id + * @return the created request + */ + @NotNull + public static PollOptionGetRequest getOption(@NotNull String pollId, @NotNull String optionId) { + return new PollOptionGetRequest(pollId, optionId); + } + + /** + * Creates an update option request + * + * @param pollId the poll id + * @return the created request + */ + @NotNull + public static PollOptionUpdateRequest updateOption(@NotNull String pollId) { + return new PollOptionUpdateRequest(pollId); + } + + /** + * Creates a delete option request + * + * @param pollId the poll id + * @param optionId the option id + * @return the created request + */ + @NotNull + public static PollOptionDeleteRequest deleteOption( + @NotNull String pollId, @NotNull String optionId) { + return new PollOptionDeleteRequest(pollId, optionId); + } + + /** + * Creates a query votes request + * + * @param pollId the poll id + * @return the created request + */ + @NotNull + public static PollVoteQueryRequest queryVotes(@NotNull String pollId) { + return new PollVoteQueryRequest(pollId); + } + + /** + * Creates a cast vote request + * + * @param messageId the message id + * @param pollId the poll id + * @return the created request + */ + @NotNull + public static PollVoteCastRequest castVote(@NotNull String messageId, @NotNull String pollId) { + return new PollVoteCastRequest(messageId, pollId); + } + + /** + * Creates a delete vote request + * + * @param messageId the message id + * @param pollId the poll id + * @param voteId the vote id + * @return the created request + */ + @NotNull + public static PollVoteDeleteRequest deleteVote( + @NotNull String messageId, @NotNull String pollId, @NotNull String voteId) { + return new PollVoteDeleteRequest(messageId, pollId, voteId); + } +} diff --git a/src/main/java/io/getstream/chat/java/services/PollService.java b/src/main/java/io/getstream/chat/java/services/PollService.java new file mode 100644 index 00000000..e22e6d27 --- /dev/null +++ b/src/main/java/io/getstream/chat/java/services/PollService.java @@ -0,0 +1,80 @@ +package io.getstream.chat.java.services; + +import io.getstream.chat.java.models.Poll.*; +import org.jetbrains.annotations.NotNull; +import retrofit2.Call; +import retrofit2.http.*; + +public interface PollService { + + // Poll CRUD endpoints + @POST("polls") + @NotNull + Call create(@NotNull @Body PollCreateRequestData pollCreateRequestData); + + @GET("polls/{poll_id}") + @NotNull + Call get(@NotNull @Path("poll_id") String pollId); + + @PUT("polls") + @NotNull + Call update(@NotNull @Body PollUpdateRequestData pollUpdateRequestData); + + @PATCH("polls/{poll_id}") + @NotNull + Call updatePartial( + @NotNull @Path("poll_id") String pollId, + @NotNull @Body PollUpdatePartialRequestData pollUpdatePartialRequestData); + + @DELETE("polls/{poll_id}") + @NotNull + Call delete(@NotNull @Path("poll_id") String pollId); + + @POST("polls/query") + @NotNull + Call query(@NotNull @Body PollQueryRequestData pollQueryRequestData); + + // PollOption CRUD endpoints + @POST("polls/{poll_id}/options") + @NotNull + Call createOption( + @NotNull @Path("poll_id") String pollId, + @NotNull @Body PollOptionCreateRequestData pollOptionCreateRequestData); + + @GET("polls/{poll_id}/options/{option_id}") + @NotNull + Call getOption( + @NotNull @Path("poll_id") String pollId, @NotNull @Path("option_id") String optionId); + + @PUT("polls/{poll_id}/options") + @NotNull + Call updateOption( + @NotNull @Path("poll_id") String pollId, + @NotNull @Body PollOptionUpdateRequestData pollOptionUpdateRequestData); + + @DELETE("polls/{poll_id}/options/{option_id}") + @NotNull + Call deleteOption( + @NotNull @Path("poll_id") String pollId, @NotNull @Path("option_id") String optionId); + + // PollVote endpoints + @POST("polls/{poll_id}/votes") + @NotNull + Call queryVotes( + @NotNull @Path("poll_id") String pollId, + @NotNull @Body PollVoteQueryRequestData pollVoteQueryRequestData); + + @POST("messages/{message_id}/polls/{poll_id}/vote") + @NotNull + Call castVote( + @NotNull @Path("message_id") String messageId, + @NotNull @Path("poll_id") String pollId, + @NotNull @Body PollVoteCastRequestData pollVoteCastRequestData); + + @DELETE("messages/{message_id}/polls/{poll_id}/vote/{vote_id}") + @NotNull + Call deleteVote( + @NotNull @Path("message_id") String messageId, + @NotNull @Path("poll_id") String pollId, + @NotNull @Path("vote_id") String voteId); +} diff --git a/src/test/java/io/getstream/chat/java/PollTest.java b/src/test/java/io/getstream/chat/java/PollTest.java new file mode 100644 index 00000000..a725a5ea --- /dev/null +++ b/src/test/java/io/getstream/chat/java/PollTest.java @@ -0,0 +1,270 @@ +package io.getstream.chat.java; + +import io.getstream.chat.java.models.FilterCondition; +import io.getstream.chat.java.models.Poll; +import io.getstream.chat.java.models.Poll.PollCreateRequestData.PollOptionInput; +import io.getstream.chat.java.models.Poll.PollUpdateRequestData.PollOptionRequest; +import io.getstream.chat.java.models.Sort; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class PollTest extends BasicTest { + + @DisplayName("Can create poll") + @Test + void whenCreatingPoll_thenCorrectName() { + String pollName = "test poll"; + PollOptionInput option1 = new PollOptionInput(); + option1.setText("Option 1"); + PollOptionInput option2 = new PollOptionInput(); + option2.setText("Option 2"); + + Poll poll = + Assertions.assertDoesNotThrow( + () -> + Poll.create() + .name(pollName) + .description("Test description") + .options(List.of(option1, option2)) + .votingVisibility(Poll.VotingVisibility.PUBLIC) + .enforceUniqueVote(false) + .request()) + .getPoll(); + Assertions.assertEquals(pollName, poll.getName()); + Assertions.assertNotNull(poll.getId()); + Assertions.assertNotNull(poll.getOptions()); + Assertions.assertEquals(2, poll.getOptions().size()); + + // Cleanup + Assertions.assertDoesNotThrow(() -> Poll.delete(poll.getId()).request()); + } + + @DisplayName("Can perform poll CRUD operations") + @Test + void whenPerformingPollCRUD_thenCorrectOperations() { + PollOptionInput option1 = new PollOptionInput(); + option1.setText("Option 1"); + PollOptionInput option2 = new PollOptionInput(); + option2.setText("Option 2"); + + // Create + String originalName = "original poll name"; + Poll created = + Assertions.assertDoesNotThrow( + () -> + Poll.create() + .name(originalName) + .description("Original description") + .options(List.of(option1, option2)) + .votingVisibility(Poll.VotingVisibility.PUBLIC) + .enforceUniqueVote(false) + .request()) + .getPoll(); + Assertions.assertEquals(originalName, created.getName()); + String pollId = created.getId(); + + pause(); + + // Read + Poll retrieved = Assertions.assertDoesNotThrow(() -> Poll.get(pollId).request()).getPoll(); + Assertions.assertEquals(pollId, retrieved.getId()); + Assertions.assertEquals(originalName, retrieved.getName()); + + // Update + String updatedName = "updated poll name"; + PollOptionRequest updatedOption1 = new PollOptionRequest(); + updatedOption1.setId(created.getOptions().get(0).getId()); + updatedOption1.setText("Updated Option 1"); + PollOptionRequest updatedOption2 = new PollOptionRequest(); + updatedOption2.setId(created.getOptions().get(1).getId()); + updatedOption2.setText("Updated Option 2"); + + Poll updated = + Assertions.assertDoesNotThrow( + () -> + Poll.update(pollId) + .name(updatedName) + .description("Updated description") + .options(List.of(updatedOption1, updatedOption2)) + .votingVisibility(Poll.VotingVisibility.PUBLIC) + .enforceUniqueVote(false) + .request()) + .getPoll(); + Assertions.assertEquals(updatedName, updated.getName()); + + pause(); + + // Update Partial + Map setFields = new HashMap<>(); + setFields.put("name", "partially updated name"); + Poll partiallyUpdated = + Assertions.assertDoesNotThrow(() -> Poll.updatePartial(pollId).set(setFields).request()) + .getPoll(); + Assertions.assertEquals("partially updated name", partiallyUpdated.getName()); + + pause(); + + // Delete + Assertions.assertDoesNotThrow(() -> Poll.delete(pollId).request()); + } + + @DisplayName("Can perform poll option CRUD operations") + @Test + void whenPerformingPollOptionCRUD_thenCorrectOperations() { + PollOptionInput option1 = new PollOptionInput(); + option1.setText("Option 1"); + + Poll created = + Assertions.assertDoesNotThrow( + () -> + Poll.create() + .name("poll with options") + .description("Test poll") + .options(List.of(option1)) + .votingVisibility(Poll.VotingVisibility.PUBLIC) + .enforceUniqueVote(false) + .request()) + .getPoll(); + String pollId = created.getId(); + + pause(); + + // Create option + Poll.PollOption newOption = + Assertions.assertDoesNotThrow(() -> Poll.createOption(pollId).text("New Option").request()) + .getPollOption(); + Assertions.assertNotNull(newOption.getId()); + Assertions.assertEquals("New Option", newOption.getText()); + + pause(); + + // Get option + Poll.PollOption retrievedOption = + Assertions.assertDoesNotThrow(() -> Poll.getOption(pollId, newOption.getId()).request()) + .getPollOption(); + Assertions.assertEquals(newOption.getId(), retrievedOption.getId()); + + // Update option + Poll.PollOption updatedOption = + Assertions.assertDoesNotThrow( + () -> + Poll.updateOption(pollId) + .id(newOption.getId()) + .text("Updated Option Text") + .request()) + .getPollOption(); + Assertions.assertEquals("Updated Option Text", updatedOption.getText()); + + pause(); + + // Delete option + Assertions.assertDoesNotThrow(() -> Poll.deleteOption(pollId, updatedOption.getId()).request()); + + // Cleanup + Assertions.assertDoesNotThrow(() -> Poll.delete(pollId).request()); + } + + @DisplayName("Can query polls") + @Test + void whenQueryingPolls_thenCorrectResults() { + PollOptionInput option1 = new PollOptionInput(); + option1.setText("Option 1"); + + Poll created = + Assertions.assertDoesNotThrow( + () -> + Poll.create() + .name("query test poll") + .description("Test poll for query") + .options(List.of(option1)) + .votingVisibility(Poll.VotingVisibility.PUBLIC) + .enforceUniqueVote(false) + .request()) + .getPoll(); + String pollId = created.getId(); + + pause(); + + // Query by ID + List polls = + Assertions.assertDoesNotThrow( + () -> + Poll.query() + .filter(FilterCondition.eq("id", pollId)) + .sorts( + List.of( + Sort.builder() + .field("created_at") + .direction(Sort.Direction.DESC) + .build())) + .limit(10) + .request()) + .getPolls(); + Assertions.assertTrue( + polls.stream().anyMatch(p -> p.getId().equals(pollId)), + "Created poll should be found in query results"); + + // Cleanup + Assertions.assertDoesNotThrow(() -> Poll.delete(pollId).request()); + } + + @DisplayName("Can create poll without ID") + @Test + void whenCreatingPollWithoutId_thenIdIsGenerated() { + PollOptionInput option1 = new PollOptionInput(); + option1.setText("Option 1"); + + Poll poll = + Assertions.assertDoesNotThrow( + () -> + Poll.create() + .name("auto id poll") + .description("Test poll") + .options(List.of(option1)) + .votingVisibility(Poll.VotingVisibility.PUBLIC) + .enforceUniqueVote(false) + .request()) + .getPoll(); + Assertions.assertNotNull(poll.getId()); + Assertions.assertFalse(poll.getId().isEmpty()); + + // Cleanup + Assertions.assertDoesNotThrow(() -> Poll.delete(poll.getId()).request()); + } + + @DisplayName("Can query poll votes") + @Test + void whenQueryingPollVotes_thenCorrectResults() { + PollOptionInput option1 = new PollOptionInput(); + option1.setText("Option 1"); + PollOptionInput option2 = new PollOptionInput(); + option2.setText("Option 2"); + + Poll created = + Assertions.assertDoesNotThrow( + () -> + Poll.create() + .name("poll for votes") + .description("Test poll") + .options(List.of(option1, option2)) + .votingVisibility(Poll.VotingVisibility.PUBLIC) + .enforceUniqueVote(false) + .request()) + .getPoll(); + String pollId = created.getId(); + + pause(); + + // Query votes (should be empty initially) + List votes = + Assertions.assertDoesNotThrow(() -> Poll.queryVotes(pollId).limit(10).request()).getVotes(); + Assertions.assertNotNull(votes); + + // Cleanup + Assertions.assertDoesNotThrow(() -> Poll.delete(pollId).request()); + } +} From c8e0b605f11348e12f3ad3663251465e67fbd85e Mon Sep 17 00:00:00 2001 From: Rafael Marinho Date: Fri, 16 Jan 2026 11:51:24 +0100 Subject: [PATCH 06/11] fix use of user_id --- .../io/getstream/chat/java/models/Poll.java | 44 +++++++++++++++++++ .../java/io/getstream/chat/java/PollTest.java | 22 +++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/getstream/chat/java/models/Poll.java b/src/main/java/io/getstream/chat/java/models/Poll.java index 6b1b0d61..279b068f 100644 --- a/src/main/java/io/getstream/chat/java/models/Poll.java +++ b/src/main/java/io/getstream/chat/java/models/Poll.java @@ -185,6 +185,14 @@ public static class PollCreateRequestData { @JsonProperty("id") private String id; + @Nullable + @JsonProperty("user_id") + private String userId; + + @Nullable + @JsonProperty("user") + private User user; + @NotNull @JsonProperty("name") private String name; @@ -274,6 +282,14 @@ public static class PollUpdateRequestData { @JsonProperty("id") private String id; + @Nullable + @JsonProperty("user") + private User user; + + @Nullable + @JsonProperty("user_id") + private String userId; + @NotNull @JsonProperty("name") private String name; @@ -354,6 +370,10 @@ protected Call generateCall(Client client) { @Getter @EqualsAndHashCode(callSuper = false) public static class PollUpdatePartialRequestData { + @Nullable + @JsonProperty("user_id") + private String userId; + @Nullable @JsonProperty("set") private Map set; @@ -436,6 +456,14 @@ protected Call generateCall(Client client) { @Getter @EqualsAndHashCode(callSuper = false) public static class PollOptionCreateRequestData { + @Nullable + @JsonProperty("user_id") + private String userId; + + @Nullable + @JsonProperty("user") + private User user; + @NotNull @JsonProperty("text") private String text; @@ -478,6 +506,14 @@ protected Call generateCall(Client client) { @Getter @EqualsAndHashCode(callSuper = false) public static class PollOptionUpdateRequestData { + @Nullable + @JsonProperty("user_id") + private String userId; + + @Nullable + @JsonProperty("user") + private User user; + @NotNull @JsonProperty("id") private String id; @@ -570,6 +606,14 @@ protected Call generateCall(Client client) { @Getter @EqualsAndHashCode(callSuper = false) public static class PollVoteCastRequestData { + @Nullable + @JsonProperty("user_id") + private String userId; + + @Nullable + @JsonProperty("user") + private User user; + @Nullable @JsonProperty("vote") private VoteData vote; diff --git a/src/test/java/io/getstream/chat/java/PollTest.java b/src/test/java/io/getstream/chat/java/PollTest.java index a725a5ea..908bd8b5 100644 --- a/src/test/java/io/getstream/chat/java/PollTest.java +++ b/src/test/java/io/getstream/chat/java/PollTest.java @@ -27,6 +27,7 @@ void whenCreatingPoll_thenCorrectName() { Assertions.assertDoesNotThrow( () -> Poll.create() + .userId(testUserRequestObject.getId()) .name(pollName) .description("Test description") .options(List.of(option1, option2)) @@ -57,6 +58,7 @@ void whenPerformingPollCRUD_thenCorrectOperations() { Assertions.assertDoesNotThrow( () -> Poll.create() + .userId(testUserRequestObject.getId()) .name(originalName) .description("Original description") .options(List.of(option1, option2)) @@ -87,6 +89,7 @@ void whenPerformingPollCRUD_thenCorrectOperations() { Assertions.assertDoesNotThrow( () -> Poll.update(pollId) + .userId(testUserRequestObject.getId()) .name(updatedName) .description("Updated description") .options(List.of(updatedOption1, updatedOption2)) @@ -102,7 +105,12 @@ void whenPerformingPollCRUD_thenCorrectOperations() { Map setFields = new HashMap<>(); setFields.put("name", "partially updated name"); Poll partiallyUpdated = - Assertions.assertDoesNotThrow(() -> Poll.updatePartial(pollId).set(setFields).request()) + Assertions.assertDoesNotThrow( + () -> + Poll.updatePartial(pollId) + .userId(testUserRequestObject.getId()) + .set(setFields) + .request()) .getPoll(); Assertions.assertEquals("partially updated name", partiallyUpdated.getName()); @@ -122,6 +130,7 @@ void whenPerformingPollOptionCRUD_thenCorrectOperations() { Assertions.assertDoesNotThrow( () -> Poll.create() + .userId(testUserRequestObject.getId()) .name("poll with options") .description("Test poll") .options(List.of(option1)) @@ -135,7 +144,12 @@ void whenPerformingPollOptionCRUD_thenCorrectOperations() { // Create option Poll.PollOption newOption = - Assertions.assertDoesNotThrow(() -> Poll.createOption(pollId).text("New Option").request()) + Assertions.assertDoesNotThrow( + () -> + Poll.createOption(pollId) + .userId(testUserRequestObject.getId()) + .text("New Option") + .request()) .getPollOption(); Assertions.assertNotNull(newOption.getId()); Assertions.assertEquals("New Option", newOption.getText()); @@ -153,6 +167,7 @@ void whenPerformingPollOptionCRUD_thenCorrectOperations() { Assertions.assertDoesNotThrow( () -> Poll.updateOption(pollId) + .userId(testUserRequestObject.getId()) .id(newOption.getId()) .text("Updated Option Text") .request()) @@ -178,6 +193,7 @@ void whenQueryingPolls_thenCorrectResults() { Assertions.assertDoesNotThrow( () -> Poll.create() + .userId(testUserRequestObject.getId()) .name("query test poll") .description("Test poll for query") .options(List.of(option1)) @@ -222,6 +238,7 @@ void whenCreatingPollWithoutId_thenIdIsGenerated() { Assertions.assertDoesNotThrow( () -> Poll.create() + .userId(testUserRequestObject.getId()) .name("auto id poll") .description("Test poll") .options(List.of(option1)) @@ -248,6 +265,7 @@ void whenQueryingPollVotes_thenCorrectResults() { Assertions.assertDoesNotThrow( () -> Poll.create() + .userId(testUserRequestObject.getId()) .name("poll for votes") .description("Test poll") .options(List.of(option1, option2)) From 72e315350c41923f4a519b1a0c0c12fe7becee14 Mon Sep 17 00:00:00 2001 From: Rafael Marinho Date: Fri, 16 Jan 2026 12:12:10 +0100 Subject: [PATCH 07/11] fix missing params --- .../io/getstream/chat/java/models/Poll.java | 46 ++++++++++++++++--- .../chat/java/services/PollService.java | 15 ++++-- .../java/io/getstream/chat/java/PollTest.java | 33 +++++++++---- 3 files changed, 75 insertions(+), 19 deletions(-) diff --git a/src/main/java/io/getstream/chat/java/models/Poll.java b/src/main/java/io/getstream/chat/java/models/Poll.java index 279b068f..5f201b37 100644 --- a/src/main/java/io/getstream/chat/java/models/Poll.java +++ b/src/main/java/io/getstream/chat/java/models/Poll.java @@ -278,7 +278,7 @@ protected Call generateCall(Client client) { @Setter @EqualsAndHashCode(callSuper = false) public static class PollUpdateRequestData { - @NotNull + @Nullable @JsonProperty("id") private String id; @@ -399,13 +399,22 @@ protected Call generateCall(Client client) { @Getter @EqualsAndHashCode(callSuper = false) - @RequiredArgsConstructor public static class PollDeleteRequest extends StreamRequest { @NotNull private String pollId; + @Nullable private String userId; + + public PollDeleteRequest(@NotNull String pollId) { + this.pollId = pollId; + } + + public PollDeleteRequest userId(@NotNull String userId) { + this.userId = userId; + return this; + } @Override protected Call generateCall(Client client) { - return client.create(PollService.class).delete(pollId); + return client.create(PollService.class).delete(pollId, userId); } } @@ -442,9 +451,16 @@ public static class PollQueryRequestData { private String prev; public static class PollQueryRequest extends StreamRequest { + @Nullable private String userId; + + public PollQueryRequest userId(@NotNull String userId) { + this.userId = userId; + return this; + } + @Override protected Call generateCall(Client client) { - return client.create(PollService.class).query(this.internalBuild()); + return client.create(PollService.class).query(this.internalBuild(), userId); } } } @@ -542,14 +558,24 @@ protected Call generateCall(Client client) { @Getter @EqualsAndHashCode(callSuper = false) - @RequiredArgsConstructor public static class PollOptionDeleteRequest extends StreamRequest { @NotNull private String pollId; @NotNull private String optionId; + @Nullable private String userId; + + public PollOptionDeleteRequest(@NotNull String pollId, @NotNull String optionId) { + this.pollId = pollId; + this.optionId = optionId; + } + + public PollOptionDeleteRequest userId(@NotNull String userId) { + this.userId = userId; + return this; + } @Override protected Call generateCall(Client client) { - return client.create(PollService.class).deleteOption(pollId, optionId); + return client.create(PollService.class).deleteOption(pollId, optionId, userId); } } @@ -587,14 +613,20 @@ public static class PollVoteQueryRequestData { public static class PollVoteQueryRequest extends StreamRequest { @NotNull private String pollId; + @Nullable private String userId; private PollVoteQueryRequest(@NotNull String pollId) { this.pollId = pollId; } + public PollVoteQueryRequest userId(@NotNull String userId) { + this.userId = userId; + return this; + } + @Override protected Call generateCall(Client client) { - return client.create(PollService.class).queryVotes(pollId, this.internalBuild()); + return client.create(PollService.class).queryVotes(pollId, this.internalBuild(), userId); } } } diff --git a/src/main/java/io/getstream/chat/java/services/PollService.java b/src/main/java/io/getstream/chat/java/services/PollService.java index e22e6d27..c57a68bb 100644 --- a/src/main/java/io/getstream/chat/java/services/PollService.java +++ b/src/main/java/io/getstream/chat/java/services/PollService.java @@ -2,6 +2,7 @@ import io.getstream.chat.java.models.Poll.*; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import retrofit2.Call; import retrofit2.http.*; @@ -28,11 +29,14 @@ Call updatePartial( @DELETE("polls/{poll_id}") @NotNull - Call delete(@NotNull @Path("poll_id") String pollId); + Call delete( + @NotNull @Path("poll_id") String pollId, @Nullable @Query("user_id") String userId); @POST("polls/query") @NotNull - Call query(@NotNull @Body PollQueryRequestData pollQueryRequestData); + Call query( + @NotNull @Body PollQueryRequestData pollQueryRequestData, + @Nullable @Query("user_id") String userId); // PollOption CRUD endpoints @POST("polls/{poll_id}/options") @@ -55,14 +59,17 @@ Call updateOption( @DELETE("polls/{poll_id}/options/{option_id}") @NotNull Call deleteOption( - @NotNull @Path("poll_id") String pollId, @NotNull @Path("option_id") String optionId); + @NotNull @Path("poll_id") String pollId, + @NotNull @Path("option_id") String optionId, + @Nullable @Query("user_id") String userId); // PollVote endpoints @POST("polls/{poll_id}/votes") @NotNull Call queryVotes( @NotNull @Path("poll_id") String pollId, - @NotNull @Body PollVoteQueryRequestData pollVoteQueryRequestData); + @NotNull @Body PollVoteQueryRequestData pollVoteQueryRequestData, + @Nullable @Query("user_id") String userId); @POST("messages/{message_id}/polls/{poll_id}/vote") @NotNull diff --git a/src/test/java/io/getstream/chat/java/PollTest.java b/src/test/java/io/getstream/chat/java/PollTest.java index 908bd8b5..100b2fa1 100644 --- a/src/test/java/io/getstream/chat/java/PollTest.java +++ b/src/test/java/io/getstream/chat/java/PollTest.java @@ -41,7 +41,8 @@ void whenCreatingPoll_thenCorrectName() { Assertions.assertEquals(2, poll.getOptions().size()); // Cleanup - Assertions.assertDoesNotThrow(() -> Poll.delete(poll.getId()).request()); + Assertions.assertDoesNotThrow( + () -> Poll.delete(poll.getId()).userId(testUserRequestObject.getId()).request()); } @DisplayName("Can perform poll CRUD operations") @@ -117,7 +118,8 @@ void whenPerformingPollCRUD_thenCorrectOperations() { pause(); // Delete - Assertions.assertDoesNotThrow(() -> Poll.delete(pollId).request()); + Assertions.assertDoesNotThrow( + () -> Poll.delete(pollId).userId(testUserRequestObject.getId()).request()); } @DisplayName("Can perform poll option CRUD operations") @@ -177,10 +179,15 @@ void whenPerformingPollOptionCRUD_thenCorrectOperations() { pause(); // Delete option - Assertions.assertDoesNotThrow(() -> Poll.deleteOption(pollId, updatedOption.getId()).request()); + Assertions.assertDoesNotThrow( + () -> + Poll.deleteOption(pollId, updatedOption.getId()) + .userId(testUserRequestObject.getId()) + .request()); // Cleanup - Assertions.assertDoesNotThrow(() -> Poll.delete(pollId).request()); + Assertions.assertDoesNotThrow( + () -> Poll.delete(pollId).userId(testUserRequestObject.getId()).request()); } @DisplayName("Can query polls") @@ -210,6 +217,7 @@ void whenQueryingPolls_thenCorrectResults() { Assertions.assertDoesNotThrow( () -> Poll.query() + .userId(testUserRequestObject.getId()) .filter(FilterCondition.eq("id", pollId)) .sorts( List.of( @@ -225,7 +233,8 @@ void whenQueryingPolls_thenCorrectResults() { "Created poll should be found in query results"); // Cleanup - Assertions.assertDoesNotThrow(() -> Poll.delete(pollId).request()); + Assertions.assertDoesNotThrow( + () -> Poll.delete(pollId).userId(testUserRequestObject.getId()).request()); } @DisplayName("Can create poll without ID") @@ -250,7 +259,8 @@ void whenCreatingPollWithoutId_thenIdIsGenerated() { Assertions.assertFalse(poll.getId().isEmpty()); // Cleanup - Assertions.assertDoesNotThrow(() -> Poll.delete(poll.getId()).request()); + Assertions.assertDoesNotThrow( + () -> Poll.delete(poll.getId()).userId(testUserRequestObject.getId()).request()); } @DisplayName("Can query poll votes") @@ -279,10 +289,17 @@ void whenQueryingPollVotes_thenCorrectResults() { // Query votes (should be empty initially) List votes = - Assertions.assertDoesNotThrow(() -> Poll.queryVotes(pollId).limit(10).request()).getVotes(); + Assertions.assertDoesNotThrow( + () -> + Poll.queryVotes(pollId) + .userId(testUserRequestObject.getId()) + .limit(10) + .request()) + .getVotes(); Assertions.assertNotNull(votes); // Cleanup - Assertions.assertDoesNotThrow(() -> Poll.delete(pollId).request()); + Assertions.assertDoesNotThrow( + () -> Poll.delete(pollId).userId(testUserRequestObject.getId()).request()); } } From 57341f2e0efcd73656b632859650544e86e9815a Mon Sep 17 00:00:00 2001 From: Rafael Marinho Date: Fri, 16 Jan 2026 12:22:17 +0100 Subject: [PATCH 08/11] fix test --- .../java/io/getstream/chat/java/PollTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/java/io/getstream/chat/java/PollTest.java b/src/test/java/io/getstream/chat/java/PollTest.java index 100b2fa1..47d6ac65 100644 --- a/src/test/java/io/getstream/chat/java/PollTest.java +++ b/src/test/java/io/getstream/chat/java/PollTest.java @@ -1,6 +1,8 @@ package io.getstream.chat.java; import io.getstream.chat.java.models.FilterCondition; +import io.getstream.chat.java.models.Message; +import io.getstream.chat.java.models.Message.MessageRequestObject; import io.getstream.chat.java.models.Poll; import io.getstream.chat.java.models.Poll.PollCreateRequestData.PollOptionInput; import io.getstream.chat.java.models.Poll.PollUpdateRequestData.PollOptionRequest; @@ -287,6 +289,21 @@ void whenQueryingPollVotes_thenCorrectResults() { pause(); + // Attach poll to a message (required before querying votes) + MessageRequestObject messageRequest = + MessageRequestObject.builder() + .text("Message with poll") + .userId(testUserRequestObject.getId()) + .build(); + messageRequest.setAdditionalField("poll_id", pollId); + Assertions.assertDoesNotThrow( + () -> + Message.send(testChannel.getType(), testChannel.getId()) + .message(messageRequest) + .request()); + + pause(); + // Query votes (should be empty initially) List votes = Assertions.assertDoesNotThrow( From fa9573326671116364dfb4e63b4c1ee338d6c0be Mon Sep 17 00:00:00 2001 From: Rafael Marinho Date: Fri, 16 Jan 2026 12:33:36 +0100 Subject: [PATCH 09/11] fix test --- src/test/java/io/getstream/chat/java/PollTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/io/getstream/chat/java/PollTest.java b/src/test/java/io/getstream/chat/java/PollTest.java index 47d6ac65..86801325 100644 --- a/src/test/java/io/getstream/chat/java/PollTest.java +++ b/src/test/java/io/getstream/chat/java/PollTest.java @@ -294,8 +294,8 @@ void whenQueryingPollVotes_thenCorrectResults() { MessageRequestObject.builder() .text("Message with poll") .userId(testUserRequestObject.getId()) + .additionalField("poll_id", pollId) .build(); - messageRequest.setAdditionalField("poll_id", pollId); Assertions.assertDoesNotThrow( () -> Message.send(testChannel.getType(), testChannel.getId()) From 2f23db0c20cace3e90ac8600d3338b96352042ca Mon Sep 17 00:00:00 2001 From: Rafael Marinho Date: Fri, 16 Jan 2026 13:08:22 +0100 Subject: [PATCH 10/11] fix test --- src/test/java/io/getstream/chat/java/PollTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/io/getstream/chat/java/PollTest.java b/src/test/java/io/getstream/chat/java/PollTest.java index 86801325..adeaa3b7 100644 --- a/src/test/java/io/getstream/chat/java/PollTest.java +++ b/src/test/java/io/getstream/chat/java/PollTest.java @@ -1,5 +1,6 @@ package io.getstream.chat.java; +import io.getstream.chat.java.models.Channel; import io.getstream.chat.java.models.FilterCondition; import io.getstream.chat.java.models.Message; import io.getstream.chat.java.models.Message.MessageRequestObject; @@ -11,11 +12,24 @@ import java.util.List; import java.util.Map; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; public class PollTest extends BasicTest { + @BeforeAll + static void setupPolls() { + Map configOverrides = new HashMap<>(); + configOverrides.put("polls", true); + + Assertions.assertDoesNotThrow( + () -> + Channel.partialUpdate(testChannel.getType(), testChannel.getId()) + .setValue("config_overrides", configOverrides) + .request()); + } + @DisplayName("Can create poll") @Test void whenCreatingPoll_thenCorrectName() { From cf328ad19c67bdd1df1bb8c7655006766dd3acbf Mon Sep 17 00:00:00 2001 From: Rafael Marinho Date: Fri, 16 Jan 2026 13:20:00 +0100 Subject: [PATCH 11/11] add channelType enable poll --- .../io/getstream/chat/java/models/ChannelType.java | 12 ++++++++++++ src/test/java/io/getstream/chat/java/PollTest.java | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/main/java/io/getstream/chat/java/models/ChannelType.java b/src/main/java/io/getstream/chat/java/models/ChannelType.java index f1441a25..82d0fde0 100644 --- a/src/main/java/io/getstream/chat/java/models/ChannelType.java +++ b/src/main/java/io/getstream/chat/java/models/ChannelType.java @@ -145,6 +145,10 @@ public class ChannelType { @JsonProperty("count_messages") private Boolean countMessages; + @Nullable + @JsonProperty("polls") + private Boolean polls; + @Data @NoArgsConstructor public static class Threshold { @@ -427,6 +431,10 @@ public static class ChannelTypeCreateRequestData { @JsonProperty("count_messages") private Boolean countMessages; + @Nullable + @JsonProperty("polls") + protected Boolean polls; + public static class ChannelTypeCreateRequest extends StreamRequest { private static final boolean DEFAULT_PUSH_NOTIFICATIONS = true; @@ -579,6 +587,10 @@ public static class ChannelTypeUpdateRequestData { @JsonProperty("count_messages") private Boolean countMessages; + @Nullable + @JsonProperty("polls") + protected Boolean polls; + public static class ChannelTypeUpdateRequest extends StreamRequest { @NotNull private String name; diff --git a/src/test/java/io/getstream/chat/java/PollTest.java b/src/test/java/io/getstream/chat/java/PollTest.java index adeaa3b7..1bb52f2c 100644 --- a/src/test/java/io/getstream/chat/java/PollTest.java +++ b/src/test/java/io/getstream/chat/java/PollTest.java @@ -1,6 +1,7 @@ package io.getstream.chat.java; import io.getstream.chat.java.models.Channel; +import io.getstream.chat.java.models.ChannelType; import io.getstream.chat.java.models.FilterCondition; import io.getstream.chat.java.models.Message; import io.getstream.chat.java.models.Message.MessageRequestObject; @@ -20,6 +21,11 @@ public class PollTest extends BasicTest { @BeforeAll static void setupPolls() { + // Enable polls on the channel type + Assertions.assertDoesNotThrow( + () -> ChannelType.update(testChannel.getType()).polls(true).request()); + + // Also enable polls on the channel via config_overrides Map configOverrides = new HashMap<>(); configOverrides.put("polls", true); @@ -28,6 +34,13 @@ static void setupPolls() { Channel.partialUpdate(testChannel.getType(), testChannel.getId()) .setValue("config_overrides", configOverrides) .request()); + + // Wait for changes to propagate + try { + Thread.sleep(6000); + } catch (InterruptedException e) { + // Do nothing + } } @DisplayName("Can create poll")