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..5f455e24 --- /dev/null +++ b/src/test/java/io/getstream/chat/java/CampaignTest.java @@ -0,0 +1,199 @@ +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()); + } +} diff --git a/src/test/java/io/getstream/chat/java/SharedLocationTest.java b/src/test/java/io/getstream/chat/java/SharedLocationTest.java index ee21a713..b25fbba8 100644 --- a/src/test/java/io/getstream/chat/java/SharedLocationTest.java +++ b/src/test/java/io/getstream/chat/java/SharedLocationTest.java @@ -36,6 +36,23 @@ 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 +60,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 +79,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 +103,13 @@ 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,11 @@ 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 +207,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 +226,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 +250,12 @@ 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 +279,10 @@ 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"); } }