diff --git a/.pubnub.yml b/.pubnub.yml index 72a596f204..c6656e98b6 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,9 +1,9 @@ name: kotlin -version: 10.6.0 +version: 11.0.0 schema: 1 scm: github.com/pubnub/kotlin files: - - build/libs/pubnub-kotlin-10.6.0-all.jar + - build/libs/pubnub-kotlin-11.0.0-all.jar sdks: - type: library @@ -23,8 +23,8 @@ sdks: - distribution-type: library distribution-repository: maven - package-name: pubnub-kotlin-10.6.0 - location: https://repo.maven.apache.org/maven2/com/pubnub/pubnub-kotlin/10.6.0/pubnub-kotlin-10.6.0.jar + package-name: pubnub-kotlin-11.0.0 + location: https://repo.maven.apache.org/maven2/com/pubnub/pubnub-kotlin/11.0.0/pubnub-kotlin-11.0.0.jar supported-platforms: supported-operating-systems: Android: @@ -121,6 +121,19 @@ sdks: license-url: https://www.apache.org/licenses/LICENSE-2.0.txt is-required: Required changelog: + - date: 2025-10-21 + version: v11.0.0 + changes: + - type: feature + text: "Added limit and offset parameters for hereNow. Number of returned users per channel by default is limited to 1000. Breaking change." + - type: bug + text: "Single-channel hereNow with includeUUIDs=false now returns channel data in the channels map for consistency with multi-channel behaviour. Previously, the channels map was empty in this scenario, forcing reliance on totalOccupancy field only. Breaking change." + - type: bug + text: "Removed possibility to use deprecated MPNS -Microsoft Push Notification Service. Breaking change." + - type: bug + text: "Added deprecation warning for GCP and old APNS PushType. ." + - type: bug + text: "In case FCM is chosen as PushType type REST query param gets fcm value instead of gcm." - date: 2025-09-11 version: v10.6.0 changes: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1556e0a38a..e223d7ec07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## v11.0.0 +October 21 2025 + +#### Added +- Added limit and offset parameters for hereNow. Number of returned users per channel by default is limited to 1000. Breaking change. + +#### Fixed +- Single-channel hereNow with includeUUIDs=false now returns channel data in the channels map for consistency with multi-channel behaviour. Previously, the channels map was empty in this scenario, forcing reliance on totalOccupancy field only. Breaking change. +- Removed possibility to use deprecated MPNS -Microsoft Push Notification Service. Breaking change. +- Added deprecation warning for GCP and old APNS PushType. . +- In case FCM is chosen as PushType type REST query param gets fcm value instead of gcm. + ## v10.6.0 September 11 2025 diff --git a/README.md b/README.md index db825345c4..748687fdb3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ You will need the publish and subscribe keys to authenticate your app. Get your com.pubnub pubnub-kotlin - 10.6.0 + 11.0.0 ``` diff --git a/gradle.properties b/gradle.properties index 86bedca76b..4ee8043e5e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ RELEASE_SIGNING_ENABLED=true SONATYPE_HOST=DEFAULT SONATYPE_AUTOMATIC_RELEASE=false GROUP=com.pubnub -VERSION_NAME=10.6.0 +VERSION_NAME=11.0.0 POM_PACKAGING=jar POM_NAME=PubNub SDK diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 64676ce99c..306bf957db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,8 +12,8 @@ ktlint = "12.1.0" dokka = "2.0.0" kotlinx_datetime = "0.6.2" kotlinx_coroutines = "1.10.2" -pubnub_js = "9.8.1" -pubnub_swift = "9.3.2" +pubnub_js = "10.1.0" +pubnub_swift = "9.3.5" [libraries] retrofit2 = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit2" } diff --git a/pubnub-gson/pubnub-gson-api/src/main/java/com/pubnub/api/java/endpoints/presence/HereNow.java b/pubnub-gson/pubnub-gson-api/src/main/java/com/pubnub/api/java/endpoints/presence/HereNow.java index b44f67387e..02305171a8 100644 --- a/pubnub-gson/pubnub-gson-api/src/main/java/com/pubnub/api/java/endpoints/presence/HereNow.java +++ b/pubnub-gson/pubnub-gson-api/src/main/java/com/pubnub/api/java/endpoints/presence/HereNow.java @@ -3,12 +3,79 @@ import com.pubnub.api.java.endpoints.Endpoint; import com.pubnub.api.models.consumer.presence.PNHereNowResult; +/** + * Obtain information about the current state of channels including a list of unique user IDs + * currently subscribed and the total occupancy count. + *

+ * + * @see com.pubnub.api.models.consumer.presence.PNHereNowResult + */ public interface HereNow extends Endpoint { + /** + * Set the channels to query for presence information. + * + * @param channels List of channel names to query. + * @return {@code this} for method chaining + * @see #channelGroups(java.util.List) + */ HereNow channels(java.util.List channels); + /** + * Set the channel groups to query for presence information. + * + * @param channelGroups List of channel group names to query. + * @return {@code this} for method chaining + * @see #channels(java.util.List) + */ HereNow channelGroups(java.util.List channelGroups); + /** + * Whether the response should include presence state information, if available. + * + * @param includeState {@code true} to include state information, {@code false} to exclude. Default: {@code false} + * @return {@code this} for method chaining + * @see #includeUUIDs(boolean) + */ HereNow includeState(boolean includeState); + /** + * Include the list of UUIDs currently present in each channel. + * + * @param includeUUIDs {@code true} to include UUID list, {@code false} for occupancy count only. Default: {@code true} + * @return {@code this} for method chaining + * @see #includeState(boolean) + * @see #limit(int) + */ HereNow includeUUIDs(boolean includeUUIDs); + + /** + * Set the maximum number of occupants to return per channel. + *

+ * The server enforces a maximum limit of 1000. Values outside this range will be rejected by the server. + *

+ * Special behavior: + *

+ * + * @param limit Maximum number of occupants to return (0-1000). Default: 1000 + * @return {@code this} for method chaining + * @see #offset(Integer) for pagination support + */ + HereNow limit(int limit); + + /** + * Set the zero-based starting index for pagination through occupants. + *

+ * Server-side validation applies: + *

+ *

+ * + * @param offset Zero-based starting position (must be >= 0). Default: null (no offset) + * @return {@code this} for method chaining + * @see #limit(int) for controlling result size + */ + HereNow offset(Integer offset); } diff --git a/pubnub-gson/pubnub-gson-docs/src/main/java/com/pubnub/docs/presence/HereNowApp.java b/pubnub-gson/pubnub-gson-docs/src/main/java/com/pubnub/docs/presence/HereNowApp.java index 13879ec6e8..0e80fe4cb6 100644 --- a/pubnub-gson/pubnub-gson-docs/src/main/java/com/pubnub/docs/presence/HereNowApp.java +++ b/pubnub-gson/pubnub-gson-docs/src/main/java/com/pubnub/docs/presence/HereNowApp.java @@ -4,6 +4,7 @@ import com.pubnub.api.PubNubException; import com.pubnub.api.UserId; +import com.pubnub.api.enums.PNLogVerbosity; import com.pubnub.api.java.PubNub; import com.pubnub.api.java.v2.PNConfiguration; import com.pubnub.api.models.consumer.presence.PNHereNowChannelData; @@ -17,6 +18,7 @@ public static void main(String[] args) throws PubNubException { // Configure PubNub instance PNConfiguration.Builder configBuilder = PNConfiguration.builder(new UserId("demoUserId"), "demo"); configBuilder.publishKey("demo"); + configBuilder.logVerbosity(PNLogVerbosity.BODY); configBuilder.secure(true); PubNub pubnub = PubNub.create(configBuilder.build()); @@ -25,6 +27,8 @@ public static void main(String[] args) throws PubNubException { pubnub.hereNow() .channels(Arrays.asList("coolChannel", "coolChannel2")) .includeUUIDs(true) + .limit(100) + .offset(10) .async(result -> { result.onSuccess((PNHereNowResult res) -> { diff --git a/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PresenceIntegrationTests.java b/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PresenceIntegrationTests.java index 66767d4623..b99367275b 100644 --- a/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PresenceIntegrationTests.java +++ b/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PresenceIntegrationTests.java @@ -1,6 +1,7 @@ package com.pubnub.api.integration; import com.google.gson.JsonObject; +import com.pubnub.api.PubNubException; import com.pubnub.api.enums.PNHeartbeatNotificationOptions; import com.pubnub.api.enums.PNStatusCategory; import com.pubnub.api.integration.util.BaseIntegrationTest; @@ -11,6 +12,7 @@ import com.pubnub.api.models.consumer.PNStatus; import com.pubnub.api.models.consumer.presence.PNHereNowChannelData; import com.pubnub.api.models.consumer.presence.PNHereNowOccupantData; +import com.pubnub.api.models.consumer.presence.PNHereNowResult; import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult; import org.awaitility.Awaitility; import org.awaitility.Durations; @@ -20,15 +22,20 @@ import org.junit.Test; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; public class PresenceIntegrationTests extends BaseIntegrationTest { @@ -382,4 +389,327 @@ public void status(@NotNull PubNub pubnub, @NotNull PNStatus status) { .until(() -> subscribeSuccess.get() && heartbeatCallsCount.get() > 2); } + @Test + public void testHereNowWithLimit() { + final AtomicBoolean success = new AtomicBoolean(); + final int testLimit = 3; + final int totalClientsCount = 6; + final String expectedChannel = RandomGenerator.get(); + + final List clients = new ArrayList() {{ + add(pubNub); + }}; + for (int i = 1; i < totalClientsCount; i++) { + clients.add(getPubNub()); + } + + for (PubNub client : clients) { + subscribeToChannel(client, expectedChannel); + } + + pause(TIMEOUT_MEDIUM); + + pubNub.hereNow() + .channels(Collections.singletonList(expectedChannel)) + .includeUUIDs(true) + .limit(testLimit) + .async((result) -> { + assertFalse(result.isFailure()); + result.onSuccess(pnHereNowResult -> { + assertEquals(1, pnHereNowResult.getTotalChannels()); + assertEquals(1, pnHereNowResult.getChannels().size()); + assertTrue(pnHereNowResult.getChannels().containsKey(expectedChannel)); + + PNHereNowChannelData channelData = pnHereNowResult.getChannels().get(expectedChannel); + assertNotNull(channelData); + assertEquals(totalClientsCount, channelData.getOccupancy()); + + // With limit=3, we should get only 3 occupants even though 6 are present + assertEquals(testLimit, channelData.getOccupants().size()); + + success.set(true); + }); + }); + + listen(success); + } + + @Test + public void testHereNowWithOffset() { + final AtomicBoolean success = new AtomicBoolean(); + final int offsetValue = 2; + final int totalClientsCount = 5; + final String expectedChannel = RandomGenerator.get(); + + final List clients = new ArrayList() {{ + add(pubNub); + }}; + for (int i = 1; i < totalClientsCount; i++) { + clients.add(getPubNub()); + } + + for (PubNub client : clients) { + subscribeToChannel(client, expectedChannel); + } + + pause(TIMEOUT_MEDIUM); + + pubNub.hereNow() + .channels(Collections.singletonList(expectedChannel)) + .includeUUIDs(true) + .offset(offsetValue) + .async((result) -> { + assertFalse(result.isFailure()); + result.onSuccess(pnHereNowResult -> { + assertEquals(1, pnHereNowResult.getTotalChannels()); + assertEquals(1, pnHereNowResult.getChannels().size()); + assertTrue(pnHereNowResult.getChannels().containsKey(expectedChannel)); + + PNHereNowChannelData channelData = pnHereNowResult.getChannels().get(expectedChannel); + assertNotNull(channelData); + assertEquals(totalClientsCount, channelData.getOccupancy()); + + // With offset=2, we should get remaining occupants (5 total - 2 skipped = 3 remaining) + assertEquals(totalClientsCount - offsetValue, channelData.getOccupants().size()); + + success.set(true); + }); + }); + + listen(success); + } + + @Test + public void testHereNowPaginationFlow() throws PubNubException { + // 8 users in channel01 + // 3 users in channel02 + final int pageSize = 3; + final int firstOffset = 0; + final int secondOffset = 3; + final int totalClientsCount = 11; + final int channel01TotalCount = 8; + final int channel02TotalCount = 3; + final String channel01 = RandomGenerator.get(); + final String channel02 = RandomGenerator.get(); + + final List clients = new ArrayList() {{ + add(pubNub); + }}; + for (int i = 1; i < channel01TotalCount; i++) { + clients.add(getPubNub()); + } + + for (PubNub client : clients) { + subscribeToChannel(client, channel01); + } + + for (int i = 0; i < channel02TotalCount; i++) { + subscribeToChannel(clients.get(i), channel02); + } + + pause(TIMEOUT_MEDIUM); + + final Set allOccupantsInChannel01 = new HashSet<>(); + + // First page + PNHereNowResult firstPage = pubNub.hereNow() + .channels(Arrays.asList(channel01, channel02)) + .includeUUIDs(true) + .limit(pageSize) + .sync(); + + assertNotNull(firstPage); + assertEquals(2, firstPage.getTotalChannels()); + + PNHereNowChannelData channel01DataPage01 = firstPage.getChannels().get(channel01); + assertNotNull(channel01DataPage01); + assertEquals(channel01TotalCount, channel01DataPage01.getOccupancy()); + assertEquals(totalClientsCount, firstPage.getTotalOccupancy()); // total occupancy across all channels + assertEquals(pageSize, channel01DataPage01.getOccupants().size()); + + PNHereNowChannelData channel02Data = firstPage.getChannels().get(channel02); + assertNotNull(channel02Data); + assertEquals(channel02TotalCount, channel02Data.getOccupancy()); + assertEquals(pageSize, channel02Data.getOccupants().size()); + + // Collect UUIDs from first page + for (PNHereNowOccupantData occupant : channel01DataPage01.getOccupants()) { + allOccupantsInChannel01.add(occupant.getUuid()); + } + + // Second page using pageSize + firstOffset + PNHereNowResult secondPage = pubNub.hereNow() + .channels(Collections.singletonList(channel01)) + .includeUUIDs(true) + .limit(pageSize) + .offset(pageSize + firstOffset) + .sync(); + + assertNotNull(secondPage); + PNHereNowChannelData channel01DataPage02 = secondPage.getChannels().get(channel01); + assertNotNull(channel01DataPage02); + assertEquals(channel01TotalCount, channel01DataPage02.getOccupancy()); + assertEquals(channel01TotalCount, secondPage.getTotalOccupancy()); // only channel01 in results + assertEquals(pageSize, channel01DataPage02.getOccupants().size()); + + + assertFalse(secondPage.getChannels().containsKey(channel02)); + + // Collect UUIDs from second page (should not overlap with first page) + for (PNHereNowOccupantData occupant : channel01DataPage02.getOccupants()) { + assertFalse("UUID " + occupant.getUuid() + " already found in first page", + allOccupantsInChannel01.contains(occupant.getUuid())); + allOccupantsInChannel01.add(occupant.getUuid()); + } + + // Third page using pageSize + secondOffset + PNHereNowResult thirdPage = pubNub.hereNow() + .channels(Collections.singletonList(channel01)) + .includeUUIDs(true) + .limit(pageSize) + .offset(pageSize + secondOffset) + .sync(); + + assertNotNull(thirdPage); + PNHereNowChannelData channel01DataPage03 = thirdPage.getChannels().get(channel01); + assertNotNull(channel01DataPage03); + assertEquals(channel01TotalCount, channel01DataPage03.getOccupancy()); + + // Should have remaining clients (8 - 3 - 3 = 2) + int expectedRemainingCount = channel01TotalCount - (pageSize * 2); + assertEquals(expectedRemainingCount, channel01DataPage03.getOccupants().size()); + + // Collect UUIDs from third page + for (PNHereNowOccupantData occupant : channel01DataPage03.getOccupants()) { + assertFalse("UUID " + occupant.getUuid() + " already found", + allOccupantsInChannel01.contains(occupant.getUuid())); + allOccupantsInChannel01.add(occupant.getUuid()); + } + + // Verify we got all unique clients + assertEquals(channel01TotalCount, allOccupantsInChannel01.size()); + } + + @Test + public void testHereNowPaginationWithEmptyChannels() { + final AtomicBoolean success = new AtomicBoolean(); + final String emptyChannel = RandomGenerator.get(); + final int pageSize = 10; + + // Don't subscribe any clients to the channel, leaving it empty + + pubNub.hereNow() + .channels(Collections.singletonList(emptyChannel)) + .includeUUIDs(true) + .limit(pageSize) + .async((result) -> { + assertFalse(result.isFailure()); + result.onSuccess(pnHereNowResult -> { + // Empty channels are still included in the response + assertEquals(1, pnHereNowResult.getTotalChannels()); + assertEquals(0, pnHereNowResult.getTotalOccupancy()); + assertEquals(1, pnHereNowResult.getChannels().size()); + + PNHereNowChannelData channelData = pnHereNowResult.getChannels().get(emptyChannel); + assertNotNull(channelData); + assertEquals(0, channelData.getOccupancy()); + assertTrue(channelData.getOccupants().isEmpty()); + + success.set(true); + }); + }); + + listen(success); + } + + @Test + public void testGlobalHereNowWithLimit() throws PubNubException { + final int testLimit = 3; + final int totalClientsCount = 6; + final String channel01 = RandomGenerator.get(); + final String channel02 = RandomGenerator.get(); + + final List clients = new ArrayList() {{ + add(pubNub); + }}; + for (int i = 1; i < totalClientsCount; i++) { + clients.add(getPubNub()); + } + + // Subscribe first 3 clients to channel01, all 6 to channel02 + for (int i = 0; i < 3; i++) { + subscribeToChannel(clients.get(i), channel01); + } + for (PubNub client : clients) { + subscribeToChannel(client, channel02); + } + + pause(TIMEOUT_MEDIUM); + + // Global hereNow (empty channels list) + PNHereNowResult result = pubNub.hereNow() + .channels(Collections.emptyList()) + .includeUUIDs(true) + .limit(testLimit) + .sync(); + + assertNotNull(result); + + // Should include at least our test channels + assertTrue(result.getTotalChannels() >= 2); + assertTrue(result.getChannels().containsKey(channel01)); + assertTrue(result.getChannels().containsKey(channel02)); + + PNHereNowChannelData channel01Data = result.getChannels().get(channel01); + PNHereNowChannelData channel02Data = result.getChannels().get(channel02); + + assertNotNull(channel01Data); + assertNotNull(channel02Data); + assertEquals(3, channel01Data.getOccupancy()); + assertEquals(6, channel02Data.getOccupancy()); + + // With limit=3, each channel should have at most 3 occupants returned + assertTrue(channel01Data.getOccupants().size() <= testLimit); + assertTrue(channel02Data.getOccupants().size() <= testLimit); + } + + @Test + public void testGlobalHereNowWithOffset() throws PubNubException { + final int offsetValue = 2; + final int totalClientsCount = 5; + final String expectedChannel = RandomGenerator.get(); + + final List clients = new ArrayList() {{ + add(pubNub); + }}; + for (int i = 1; i < totalClientsCount; i++) { + clients.add(getPubNub()); + } + + for (PubNub client : clients) { + subscribeToChannel(client, expectedChannel); + } + + pause(TIMEOUT_MEDIUM); + + // Global hereNow with offset + PNHereNowResult result = pubNub.hereNow() + .channels(Collections.emptyList()) + .includeUUIDs(true) + .offset(offsetValue) + .sync(); + + assertNotNull(result); + + // Should include at least our test channel + assertTrue(result.getTotalChannels() >= 1); + assertTrue(result.getChannels().containsKey(expectedChannel)); + + PNHereNowChannelData channelData = result.getChannels().get(expectedChannel); + assertNotNull(channelData); + assertEquals(totalClientsCount, channelData.getOccupancy()); + + // With offset=2, we should get remaining occupants + assertTrue(channelData.getOccupants().size() <= totalClientsCount - offsetValue); + } } diff --git a/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PushIntegrationTest.java b/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PushIntegrationTest.java index ec3b0a9f25..7e2ca4782b 100644 --- a/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PushIntegrationTest.java +++ b/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PushIntegrationTest.java @@ -44,8 +44,7 @@ protected void onBefore() { public void testEnumNames() { assertEquals("apns", PNPushType.APNS.toString()); assertEquals("gcm", PNPushType.GCM.toString()); - assertEquals("gcm", PNPushType.FCM.toString()); - assertEquals("mpns", PNPushType.MPNS.toString()); + assertEquals("fcm", PNPushType.FCM.toString()); assertEquals("apns2", PNPushType.APNS2.toString()); } diff --git a/pubnub-gson/pubnub-gson-impl/src/main/java/com/pubnub/internal/java/endpoints/presence/HereNowImpl.java b/pubnub-gson/pubnub-gson-impl/src/main/java/com/pubnub/internal/java/endpoints/presence/HereNowImpl.java index 2d6f54d462..2bdab9bd6c 100644 --- a/pubnub-gson/pubnub-gson-impl/src/main/java/com/pubnub/internal/java/endpoints/presence/HereNowImpl.java +++ b/pubnub-gson/pubnub-gson-impl/src/main/java/com/pubnub/internal/java/endpoints/presence/HereNowImpl.java @@ -16,10 +16,13 @@ @Setter @Accessors(chain = true, fluent = true) public class HereNowImpl extends PassthroughEndpoint implements HereNow { + public static final int MAX_CHANNEL_OCCUPANTS_LIMIT = 1000; private List channels = new ArrayList<>(); private List channelGroups = new ArrayList<>(); private boolean includeState = false; private boolean includeUUIDs = true; + private int limit = MAX_CHANNEL_OCCUPANTS_LIMIT; + private Integer offset = null; public HereNowImpl(PubNub pubnub) { super(pubnub); @@ -32,7 +35,9 @@ protected Endpoint createRemoteAction() { channels, channelGroups, includeState, - includeUUIDs + includeUUIDs, + limit, + offset ); } } diff --git a/pubnub-kotlin/pubnub-kotlin-api/config/ktlint/baseline.xml b/pubnub-kotlin/pubnub-kotlin-api/config/ktlint/baseline.xml index 7f53a42813..de677237b5 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/config/ktlint/baseline.xml +++ b/pubnub-kotlin/pubnub-kotlin-api/config/ktlint/baseline.xml @@ -23,16 +23,25 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + @@ -42,6 +51,9 @@ + + + @@ -66,17 +78,5 @@ - - - - - - - - - - - - diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/PubNubImpl.kt b/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/PubNubImpl.kt index 43a2d06817..86d73e2a06 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/PubNubImpl.kt +++ b/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/PubNubImpl.kt @@ -353,14 +353,18 @@ class PubNubImpl(private val pubNubObjC: KMPPubNub) : PubNub { channels: List, channelGroups: List, includeState: Boolean, - includeUUIDs: Boolean + includeUUIDs: Boolean, + limit: Int, + offset: Int? ): HereNow { return HereNowImpl( pubnub = pubNubObjC, channels = channels, channelGroups = channelGroups, includeState = includeState, - includeUUIDs = includeUUIDs + includeUUIDs = includeUUIDs, + limit = limit, + offset = offset ) } diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.ios.kt b/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.ios.kt index e1043b2a3d..87ccd1334c 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.ios.kt +++ b/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.ios.kt @@ -27,7 +27,9 @@ class HereNowImpl( private val channels: List, private val channelGroups: List, private val includeState: Boolean, - private val includeUUIDs: Boolean + private val includeUUIDs: Boolean, + private val limit: Int = 1000, + private val offset: Int? = null, ) : HereNow { override fun async(callback: Consumer>) { pubnub.hereNowWithChannels( @@ -35,6 +37,9 @@ class HereNowImpl( channelGroups = channelGroups, includeState = includeState, includeUUIDs = includeUUIDs, + // todo pass limit and offset once available + // limit = limit, + // offset = offset, onSuccess = callback.onSuccessHandler { PNHereNowResult( totalChannels = it?.totalChannels()?.toInt() ?: 0, diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/PubNub.kt b/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/PubNub.kt index ee388d74ce..6086654a6f 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/PubNub.kt +++ b/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/PubNub.kt @@ -171,6 +171,8 @@ expect interface PubNub { channelGroups: List = emptyList(), includeState: Boolean = false, includeUUIDs: Boolean = true, + limit: Int = 1000, + offset: Int? = null, ): HereNow fun whereNow(uuid: String = configuration.userId.value): WhereNow diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/Pubnub.d.kt b/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/Pubnub.d.kt index b31983b387..0d1bec22be 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/Pubnub.d.kt +++ b/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/Pubnub.d.kt @@ -1307,16 +1307,6 @@ open external class PubNub(config: Any /* UUID | UserId */) { var isSilent: Boolean } - interface MPNSNotificationPayload : BaseNotificationPayload { - var backContent: String? - - var backTitle: String? - - var count: Number? - - var type: String? - } - interface FCMNotificationPayload : BaseNotificationPayload { var isSilent: Boolean var icon: String? @@ -1326,7 +1316,6 @@ open external class PubNub(config: Any /* UUID | UserId */) { interface `T$37` { var apns: Any? - var mpns: Any? var fcm: Any? } @@ -1344,7 +1333,6 @@ open external class PubNub(config: Any /* UUID | UserId */) { var body: String? var apns: APNSNotificationPayload - var mpns: MPNSNotificationPayload var fcm: FCMNotificationPayload fun buildPayload(platforms: Array): Any? diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/com/pubnub/api/PubNubImpl.kt b/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/com/pubnub/api/PubNubImpl.kt index 6fd8a1444b..e980f510bd 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/com/pubnub/api/PubNubImpl.kt +++ b/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/com/pubnub/api/PubNubImpl.kt @@ -354,11 +354,14 @@ class PubNubImpl(val jsPubNub: PubNubJs) : PubNub { channels: List, channelGroups: List, includeState: Boolean, - includeUUIDs: Boolean + includeUUIDs: Boolean, + limit: Int, + offset: Int? ): HereNow { return HereNowImpl( jsPubNub, createJsObject { + // todo handle limit and offset this.channels = channels.toTypedArray() this.channelGroups = channelGroups.toTypedArray() this.includeState = includeState diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/PubNub.kt b/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/PubNub.kt index e0fb0a88e9..8fbfcb8001 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/PubNub.kt +++ b/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/PubNub.kt @@ -492,7 +492,7 @@ actual interface PubNub : StatusEmitter, EventEmitter { /** * Enable push notifications on provided set of channels. * - * @param pushType Accepted values: FCM, APNS, MPNS, APNS2. + * @param pushType Accepted values: FCM, APNS2. * @see [PNPushType] * @param channels Channels to add push notifications to. * @param deviceId The device ID (token) to associate with push notifications. @@ -512,7 +512,7 @@ actual interface PubNub : StatusEmitter, EventEmitter { /** * Request a list of all channels on which push notifications have been enabled using specified [ListPushProvisions.deviceId]. * - * @param pushType Accepted values: FCM, APNS, MPNS, APNS2. @see [PNPushType] + * @param pushType Accepted values: FCM, APNS2. @see [PNPushType] * @param deviceId The device ID (token) to associate with push notifications. * @param environment Environment within which device should manage list of channels with enabled notifications * (works only if [pushType] set to [PNPushType.APNS2]). @@ -529,7 +529,7 @@ actual interface PubNub : StatusEmitter, EventEmitter { /** * Disable push notifications on provided set of channels. * - * @param pushType Accepted values: FCM, APNS, MPNS, APNS2. @see [PNPushType] + * @param pushType Accepted values: FCM, APNS2. @see [PNPushType] * @param channels Channels to remove push notifications from. * @param deviceId The device ID (token) associated with push notifications. * @param environment Environment within which device should manage list of channels with enabled notifications @@ -548,7 +548,7 @@ actual interface PubNub : StatusEmitter, EventEmitter { /** * Disable push notifications from all channels registered with the specified [RemoveAllPushChannelsForDevice.deviceId]. * - * @param pushType Accepted values: FCM, APNS, MPNS, APNS2. @see [PNPushType] + * @param pushType Accepted values: FCM, APNS2. @see [PNPushType] * @param deviceId The device ID (token) to associate with push notifications. * @param environment Environment within which device should manage list of channels with enabled notifications * (works only if [pushType] set to [PNPushType.APNS2]). @@ -761,19 +761,26 @@ actual interface PubNub : StatusEmitter, EventEmitter { * currently subscribed to the channel and the total occupancy count of the channel. * * @param channels The channels to get the 'here now' details of. - * Leave empty for a 'global her now'. + * Leave empty for a 'global here now'. * @param channelGroups The channel groups to get the 'here now' details of. - * Leave empty for a 'global her now'. + * Leave empty for a 'global here now'. * @param includeState Whether the response should include presence state information, if available. * Defaults to `false`. * @param includeUUIDs Whether the response should include UUIDs od connected clients. * Defaults to `true`. + * @param limit Maximum number of occupants to return per channel. Valid range: 0-1000. + * - Default: 1000 + * - Use 0 to get occupancy counts without user details + * @param offset Zero-based starting index for pagination. Returns occupants starting from this position in the list. Must be >= 0. + * - Default: null (no offset) */ actual fun hereNow( channels: List, channelGroups: List, includeState: Boolean, includeUUIDs: Boolean, + limit: Int, + offset: Int?, ): HereNow /** diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt b/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt index 6594e1616c..d71c7fa14c 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt +++ b/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt @@ -11,4 +11,6 @@ actual interface HereNow : Endpoint { val channelGroups: List val includeState: Boolean val includeUUIDs: Boolean + val limit: Int + val offset: Int? } diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/nonJvm/kotlin/com/pubnub/api/PubNub.nonJvm.kt b/pubnub-kotlin/pubnub-kotlin-api/src/nonJvm/kotlin/com/pubnub/api/PubNub.nonJvm.kt index 20dc62ab1a..09fb611dda 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/src/nonJvm/kotlin/com/pubnub/api/PubNub.nonJvm.kt +++ b/pubnub-kotlin/pubnub-kotlin-api/src/nonJvm/kotlin/com/pubnub/api/PubNub.nonJvm.kt @@ -158,7 +158,9 @@ actual interface PubNub { channels: List, channelGroups: List, includeState: Boolean, - includeUUIDs: Boolean + includeUUIDs: Boolean, + limit: Int, + offset: Int? ): HereNow actual fun whereNow(uuid: String): WhereNow diff --git a/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/PubNubError.kt b/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/PubNubError.kt index e855bd4fe3..d034dd563f 100644 --- a/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/PubNubError.kt +++ b/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/PubNubError.kt @@ -239,6 +239,7 @@ enum class PubNubError(private val code: Int, val message: String) { 181, "Channel and/or ChannelGroup contains empty string which is not allowed.", ), + ; override fun toString(): String { diff --git a/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/enums/PNPushType.kt b/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/enums/PNPushType.kt index 8019a8ee7b..d8f5c5d181 100644 --- a/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/enums/PNPushType.kt +++ b/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/enums/PNPushType.kt @@ -6,9 +6,14 @@ enum class PNPushType(private val value: String) { message = "GCM is deprecated. Use FCM instead." ) GCM("gcm"), + + @Deprecated( + replaceWith = ReplaceWith("APNS2"), + message = "APNS is deprecated. Use APNS2 instead." + ) APNS("apns"), - MPNS("mpns"), - FCM("gcm"), + + FCM("fcm"), APNS2("apns2"), ; diff --git a/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadHelper.kt b/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadHelper.kt index ebff6b0f84..54e8427ce3 100644 --- a/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadHelper.kt +++ b/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadHelper.kt @@ -12,7 +12,6 @@ class PushPayloadHelper { ) var fcmPayload: FCMPayload? = null var fcmPayloadV2: FCMPayloadV2? = null - var mpnsPayload: MPNSPayload? = null var apnsPayload: APNSPayload? = null fun build(): Map { @@ -38,13 +37,6 @@ class PushPayloadHelper { } } } - mpnsPayload?.let { - it.toMap().run { - if (isNotEmpty()) { - put("pn_mpns", this) - } - } - } commonPayload?.let { putAll(it) } } } @@ -131,26 +123,6 @@ class PushPayloadHelper { } } - class MPNSPayload : PushPayloadSerializer { - var count: Int? = null - var backTitle: String? = null - var title: String? = null - var backContent: String? = null - var type: String? = null - var custom: Map? = null - - override fun toMap(): Map { - return mutableMapOf().apply { - count?.let { put("count", it) } - backTitle?.let { put("back_title", it) } - title?.let { put("title", it) } - backContent?.let { put("back_content", it) } - type?.let { put("type", it) } - custom?.let { putAll(it) } - } - } - } - @Deprecated( replaceWith = ReplaceWith("FCMPayloadV2"), message = "The legacy GCM/FCM payload is deprecated and will" + diff --git a/pubnub-kotlin/pubnub-kotlin-docs/src/main/kotlin/com/pubnub/docs/presence/HereNowMain.kt b/pubnub-kotlin/pubnub-kotlin-docs/src/main/kotlin/com/pubnub/docs/presence/HereNowMain.kt index 899ca616e9..512c99a01e 100644 --- a/pubnub-kotlin/pubnub-kotlin-docs/src/main/kotlin/com/pubnub/docs/presence/HereNowMain.kt +++ b/pubnub-kotlin/pubnub-kotlin-docs/src/main/kotlin/com/pubnub/docs/presence/HereNowMain.kt @@ -42,7 +42,9 @@ fun singleChannelHereNow(pubnub: PubNub, channel: String) { println("\n# Basic hereNow for single channel: $channel") pubnub.hereNow( - channels = listOf(channel) + channels = listOf(channel), + limit = 100, + offset = 10 ).async { result -> result.onSuccess { response -> println("SUCCESS: Retrieved presence information") diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PresenceIntegrationTests.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PresenceIntegrationTests.kt index 3834dd491b..b3a9bdd384 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PresenceIntegrationTests.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PresenceIntegrationTests.kt @@ -1,6 +1,7 @@ package com.pubnub.api.integration import com.pubnub.api.PubNub +import com.pubnub.api.PubNubException import com.pubnub.api.callbacks.SubscribeCallback import com.pubnub.api.enums.PNHeartbeatNotificationOptions import com.pubnub.api.enums.PNStatusCategory @@ -12,6 +13,7 @@ import com.pubnub.test.CommonUtils.randomValue import com.pubnub.test.asyncRetry import com.pubnub.test.await import com.pubnub.test.listen +import com.pubnub.test.subscribeNonBlocking import com.pubnub.test.subscribeToBlocking import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull @@ -284,4 +286,595 @@ class PresenceIntegrationTests : BaseIntegrationTest() { Assert.assertNotNull(interceptedUrl) assertTrue(interceptedUrl!!.queryParameterNames.contains("ee")) } + + @Test + fun testHereNowWithLimit() { + val testLimit = 3 + val totalClientsCount = 6 + val expectedChannel = randomChannel() + + val clients = + mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList()) + } + + clients.forEach { + it.subscribeNonBlocking(expectedChannel) + } + Thread.sleep(2000) + + pubnub.hereNow( + channels = listOf(expectedChannel), + includeUUIDs = true, + limit = testLimit, + ).asyncRetry { result -> + assertFalse(result.isFailure) + result.onSuccess { + assertEquals(1, it.totalChannels) + assertEquals(1, it.channels.size) + assertTrue(it.channels.containsKey(expectedChannel)) + + val channelData = it.channels[expectedChannel]!! + assertEquals(totalClientsCount, channelData.occupancy) + + // With limit=3, we should get only 3 occupants even though 6 are present + assertEquals(testLimit, channelData.occupants.size) + } + } + } + + @Test + fun testHereNowWithStartFrom() { + val offsetValue = 2 + val totalClientsCount = 5 + val expectedChannel = randomChannel() + + val clients = + mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList()) + } + + clients.forEach { + it.subscribeNonBlocking(expectedChannel) + } + Thread.sleep(2000) + + pubnub.hereNow( + channels = listOf(expectedChannel), + includeUUIDs = true, + offset = offsetValue, + ).asyncRetry { result -> + assertFalse(result.isFailure) + result.onSuccess { + assertEquals(1, it.totalChannels) + assertEquals(1, it.channels.size) + assertTrue(it.channels.containsKey(expectedChannel)) + + val channelData = it.channels[expectedChannel]!! + assertEquals(totalClientsCount, channelData.occupancy) + + // With offset=2, we should get remaining occupants (5 total - 2 skipped = 3 remaining) + assertEquals(totalClientsCount - offsetValue, channelData.occupants.size) + } + } + } + + @Test + fun testHereNowWithStartFromIncludeUUIDSisFalse() { + val offsetValue = 2 + val totalClientsCount = 5 + val expectedChannel = randomChannel() + + val clients = + mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList()) + } + + clients.forEach { + it.subscribeNonBlocking(expectedChannel) + } + Thread.sleep(2000) + + pubnub.hereNow( + channels = listOf(expectedChannel), + includeUUIDs = false, + offset = offsetValue, + ).asyncRetry { result -> + assertFalse(result.isFailure) + result.onSuccess { + assertEquals(1, it.totalChannels) + assertEquals(1, it.channels.size) // Channel data is always present (consistent with multi-channel) + assertEquals(totalClientsCount, it.totalOccupancy) + + // Verify channel data is present with occupancy but no occupants list + val channelData = it.channels[expectedChannel]!! + assertEquals(totalClientsCount, channelData.occupancy) + assertEquals(0, channelData.occupants.size) // occupants list is empty when includeUUIDs = false + } + } + } + + @Test + fun testHereNowPaginationFlow() { + // 8 users in channel01 + // 3 users in channel02 + val pageSize = 3 + val firstPageOffset = 0 + val secondPageOffset = 3 + val totalClientsCount = 11 + val channel01TotalCount = 8 + val channel02TotalCount = 3 + val channel01 = randomChannel() + val channel02 = randomChannel() + + val clients = + mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(channel01TotalCount - 1).toList()) + } + + clients.forEach { + it.subscribeNonBlocking(channel01) + } + + clients.take(3).forEach { + it.subscribeNonBlocking(channel02) + } + + Thread.sleep(2000) + + val allOccupantsInChannel01 = mutableSetOf() + + // First page + val firstPage = pubnub.hereNow( + channels = listOf(channel01, channel02), + includeUUIDs = true, + limit = pageSize, + ).sync()!! + + assertEquals(2, firstPage.totalChannels) + val channel01DataPage01 = firstPage.channels[channel01]!! + assertEquals(channel01TotalCount, channel01DataPage01.occupancy) + assertEquals(totalClientsCount, firstPage.totalOccupancy) // this is totalOccupancy in all pages + assertEquals(pageSize, channel01DataPage01.occupants.size) + val channel02Data = firstPage.channels[channel02]!! + assertEquals(channel02TotalCount, channel02Data.occupancy) + assertEquals(pageSize, channel02Data.occupants.size) + + // Collect UUIDs from first page + channel01DataPage01.occupants.forEach { allOccupantsInChannel01.add(it.uuid) } + + // Second page using pageSize + firstPageOffset + val secondPage = pubnub.hereNow( + channels = listOf(channel01), + includeUUIDs = true, + limit = pageSize, + offset = pageSize + firstPageOffset, + ).sync()!! + + val channel01DataPage02 = secondPage.channels[channel01]!! + assertEquals(channel01TotalCount, channel01DataPage02.occupancy) + assertEquals( + channel01TotalCount, + secondPage.totalOccupancy + ) // we get result only from channel01 because there is no more result for channel02 + assertEquals(pageSize, channel01DataPage02.occupants.size) + + assertFalse(secondPage.channels.containsKey(channel02)) + + // Collect UUIDs from second page (should not overlap with first page) + channel01DataPage02.occupants.forEach { + assertFalse("UUID ${it.uuid} already found in first page", allOccupantsInChannel01.contains(it.uuid)) + allOccupantsInChannel01.add(it.uuid) + } + + // Third page using pageSize + secondPageOffset + val thirdPage = pubnub.hereNow( + channels = listOf(channel01), + includeUUIDs = true, + limit = pageSize, + offset = pageSize + secondPageOffset, + ).sync()!! + + val channel01DataPage03 = thirdPage.channels[channel01]!! + assertEquals(channel01TotalCount, channel01DataPage03.occupancy) + + // Should have remaining clients (8 - 3 - 3 = 2) + val expectedRemainingCount = channel01TotalCount - (pageSize * 2) + assertEquals(expectedRemainingCount, channel01DataPage03.occupants.size) + + // Collect UUIDs from third page + channel01DataPage03.occupants.forEach { + assertFalse("UUID ${it.uuid} already found", allOccupantsInChannel01.contains(it.uuid)) + allOccupantsInChannel01.add(it.uuid) + } + + // Verify we got all unique clients + assertEquals(channel01TotalCount, allOccupantsInChannel01.size) + } + + @Test + fun testHereNowPaginationFlowIncludeUUIDSisFalse() { + // 8 users in channel01 + // 3 users in channel02 + val pageSize = 3 + val totalClientsCount = 11 + val channel01TotalCount = 8 + val channel02TotalCount = 3 + val channel01 = randomChannel() + val channel02 = randomChannel() + + val clients = + mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(channel01TotalCount - 1).toList()) + } + + clients.forEach { + it.subscribeNonBlocking(channel01) + } + + clients.take(3).forEach { + it.subscribeNonBlocking(channel02) + } + + Thread.sleep(2000) + + // First page + val firstPage = pubnub.hereNow( + channels = listOf(channel01, channel02), + includeUUIDs = false, + limit = pageSize, + ).sync()!! + + assertEquals(2, firstPage.totalChannels) + val channel01Data = firstPage.channels[channel01]!! + assertEquals(channel01TotalCount, channel01Data.occupancy) + assertEquals(0, channel01Data.occupants.size) + assertEquals(totalClientsCount, firstPage.totalOccupancy) // this is totalOccupancy in all pages + val channel02Data = firstPage.channels[channel02]!! + assertEquals(channel02TotalCount, channel02Data.occupancy) + assertEquals(0, channel02Data.occupants.size) + } + + @Test + fun testHereNowWithLimit0() { + val limit = 0 + val totalClientsCount = 5 + val expectedChannel = randomChannel() + + // Subscribe multiple clients to the channel + val clients = + mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList()) + } + + clients.forEach { + it.subscribeNonBlocking(expectedChannel) + } + Thread.sleep(2000) + + // Query with limit=0 to get occupancy without occupant details + pubnub.hereNow( + channels = listOf(expectedChannel), + includeUUIDs = true, + limit = limit, + ).asyncRetry { result -> + assertFalse(result.isFailure) + result.onSuccess { + assertEquals(1, it.totalChannels) + val channelData = it.channels[expectedChannel]!! + + // Occupancy should reflect actual client count + assertEquals(totalClientsCount, channelData.occupancy) + + // With limit=0, occupants list should be empty + assertEquals(0, channelData.occupants.size) + } + } + } + + @Test + fun testHereNowMultipleChannelsWithLimit0() { + val limit = 0 + val totalClientsCount = 5 + val channel01 = randomChannel() + val channel02 = randomChannel() + + // Subscribe multiple clients to both channels + val clients = + mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList()) + } + + clients.forEach { + it.subscribeNonBlocking(channel01) + it.subscribeNonBlocking(channel02) + } + Thread.sleep(2000) + + // Query with limit=0 to get occupancy without occupant details for multiple channels + pubnub.hereNow( + channels = listOf(channel01, channel02), + includeUUIDs = true, + limit = limit, + ).asyncRetry { result -> + assertFalse(result.isFailure) + result.onSuccess { + assertEquals(2, it.totalChannels) + + val channel01Data = it.channels[channel01]!! + val channel02Data = it.channels[channel02]!! + + // Occupancy should reflect actual client count for both channels + assertEquals(totalClientsCount, channel01Data.occupancy) + assertEquals(totalClientsCount, channel02Data.occupancy) + + // With limit=0, occupants list should be empty for both channels + assertEquals(0, channel01Data.occupants.size) + assertEquals(0, channel02Data.occupants.size) + } + } + } + + @Test + fun testGlobalHereNowWithLimit0() { + val limit = 0 + val totalClientsCount = 4 + val expectedChannel = randomChannel() + + // Subscribe multiple clients to the channel + val clients = + mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList()) + } + + clients.forEach { + it.subscribeNonBlocking(expectedChannel) + } + Thread.sleep(2000) + + // Global hereNow with limit=0 + val result = pubnub.hereNow( + channels = emptyList(), + includeUUIDs = true, + limit = limit, + ).sync()!! + + // Should include at least our test channel + assertTrue(result.totalChannels >= 1) + assertTrue(result.channels.containsKey(expectedChannel)) + + val channelData = result.channels[expectedChannel]!! + assertEquals(totalClientsCount, channelData.occupancy) + + // With limit=0, occupants list should be empty + assertEquals(0, channelData.occupants.size) + } + + @Test + fun testGlobalHereNowWithLimit() { + val testLimit = 3 + val totalClientsCount = 6 + val channel01 = randomChannel() + val channel02 = randomChannel() + + val clients = + mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList()) + } + + // Subscribe first 3 clients to channel01, all 6 to channel02 + clients.take(3).forEach { + it.subscribeNonBlocking(channel01) + } + clients.forEach { + it.subscribeNonBlocking(channel02) + } + Thread.sleep(2000) + + // Global hereNow (no channels specified) + val result = pubnub.hereNow( + channels = emptyList(), + includeUUIDs = true, + limit = testLimit, + ).sync()!! + + // Should include at least our test channels + assertTrue(result.totalChannels >= 2) + assertTrue(result.channels.containsKey(channel01)) + assertTrue(result.channels.containsKey(channel02)) + + val channel01Data = result.channels[channel01]!! + val channel02Data = result.channels[channel02]!! + + assertEquals(3, channel01Data.occupancy) + assertEquals(6, channel02Data.occupancy) + + // With limit=3, each channel should have at most 3 occupants returned + assertTrue(channel01Data.occupants.size <= testLimit) + assertTrue(channel02Data.occupants.size <= testLimit) + } + + @Test + fun testGlobalHereNowWithOffset() { + val offsetValue = 2 + val totalClientsCount = 5 + val expectedChannel = randomChannel() + + val clients = + mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList()) + } + + clients.forEach { + it.subscribeNonBlocking(expectedChannel) + } + Thread.sleep(2000) + + // Global hereNow with offset + val result = pubnub.hereNow( + channels = emptyList(), + includeUUIDs = true, + offset = offsetValue, + ).sync()!! + + // Should include at least our test channel + assertTrue(result.totalChannels >= 1) + assertTrue(result.channels.containsKey(expectedChannel)) + + val channelData = result.channels[expectedChannel]!! + assertEquals(totalClientsCount, channelData.occupancy) + + // With offset=2, we should get remaining occupants + assertTrue(channelData.occupants.size <= totalClientsCount - offsetValue) + } + + @Test + fun testGlobalHereNowWithNoActiveChannels() { + // Don't subscribe any clients, making it a truly empty global query + // Wait a bit to ensure no residual presence state from other tests + + val result = pubnub.hereNow( + channels = emptyList(), + includeUUIDs = true, + limit = 10, + ).sync()!! + + // Should have no channels + // Note: In a shared test environment, there might be residual presence state + assertTrue(result.totalOccupancy >= 0) + } + + @Test + fun testHereNowWithChannelGroupPagination() { + val testLimit = 3 + val totalClientsCount = 6 + val channelGroupName = randomValue() + val channel01 = randomChannel() + val channel02 = randomChannel() + + // Create channel group and add channels to it + pubnub.addChannelsToChannelGroup( + channels = listOf(channel01, channel02), + channelGroup = channelGroupName, + ).sync() + + Thread.sleep(1000) // Wait for channel group to be created + + // Subscribe clients to the channels + val clients = mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList()) + } + + // Subscribe all clients to channel01, first 3 to channel02 + clients.forEach { + it.subscribeNonBlocking(channel01) + } + clients.take(3).forEach { + it.subscribeNonBlocking(channel02) + } + + Thread.sleep(2000) // Wait for presence to register + + // Query hereNow with channel group and limit + val result = pubnub.hereNow( + channelGroups = listOf(channelGroupName), + includeUUIDs = true, + limit = testLimit, + ).sync()!! + + // Verify results + assertEquals(2, result.totalChannels) + assertTrue(result.channels.containsKey(channel01)) + assertTrue(result.channels.containsKey(channel02)) + + val channel01Data = result.channels[channel01]!! + val channel02Data = result.channels[channel02]!! + + // Verify occupancy counts + assertEquals(totalClientsCount, channel01Data.occupancy) + assertEquals(3, channel02Data.occupancy) + + // Verify pagination: with limit=3, each channel should return at most 3 occupants + assertTrue(channel01Data.occupants.size <= testLimit) + assertTrue(channel02Data.occupants.size <= testLimit) + assertEquals(testLimit, channel01Data.occupants.size) // channel01 has 6 users, should return 3 + assertEquals(testLimit, channel02Data.occupants.size) // channel02 has 3 users, should return 3 + + // Test with offset + val resultWithOffset = pubnub.hereNow( + channels = emptyList(), + channelGroups = listOf(channelGroupName), + includeUUIDs = true, + limit = testLimit, + offset = 2, + ).sync()!! + + val channel01DataWithOffset = resultWithOffset.channels[channel01]!! + assertEquals(totalClientsCount, channel01DataWithOffset.occupancy) + // With offset=2 and limit=3, we should get 3 occupants (skipping first 2) + assertEquals(testLimit, channel01DataWithOffset.occupants.size) + + // Cleanup: remove channel group + pubnub.deleteChannelGroup( + channelGroup = channelGroupName, + ).sync() + } + + @Test + fun testHereNowWithLimitAbove1000() { + val limitAboveMax = 2000 + val totalClientsCount = 5 + val expectedChannel = randomChannel() + + val clients = + mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList()) + } + + clients.forEach { + it.subscribeNonBlocking(expectedChannel) + } + Thread.sleep(2000) + + // This should not throw a client-side validation error + // Server will validate the limit and respond accordingly + pubnub.hereNow( + channels = listOf(expectedChannel), + includeUUIDs = true, + limit = limitAboveMax, + ).asyncRetry { result -> + assertTrue(result.isFailure) + result.onFailure { exception: PubNubException -> + assertTrue(exception.message!!.contains("Cannot return more than 1000 uuids at a time")) + } + } + } + + @Test + fun testHereNowWithLimitBelow0() { + val limitBelow0 = -1 + val totalClientsCount = 5 + val expectedChannel = randomChannel() + + val clients = + mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList()) + } + + clients.forEach { + it.subscribeNonBlocking(expectedChannel) + } + Thread.sleep(2000) + + pubnub.hereNow( + channels = listOf(expectedChannel), + includeUUIDs = true, + limit = limitBelow0, + ).asyncRetry { result -> + assertTrue(result.isFailure) + result.onFailure { exception: PubNubException -> + assertTrue(exception.message!!.contains("Limit must be greater than or equal to 0")) + } + } + } } diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushIntegrationTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushIntegrationTest.kt index 2f14c6a75b..fdb2b814ca 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushIntegrationTest.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushIntegrationTest.kt @@ -26,8 +26,7 @@ class PushIntegrationTest : BaseIntegrationTest() { @Test fun testEnumNames() { assertEquals("apns", PNPushType.APNS.toParamString()) - assertEquals("gcm", PNPushType.FCM.toParamString()) - assertEquals("mpns", PNPushType.MPNS.toParamString()) + assertEquals("fcm", PNPushType.FCM.toParamString()) assertEquals("apns2", PNPushType.APNS2.toParamString()) } diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushPayloadHelperIntegrationTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushPayloadHelperIntegrationTest.kt deleted file mode 100644 index 9826b330f0..0000000000 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushPayloadHelperIntegrationTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.pubnub.api.integration - -import com.google.gson.Gson -import com.google.gson.JsonObject -import com.pubnub.api.PubNub -import com.pubnub.api.callbacks.SubscribeCallback -import com.pubnub.api.models.consumer.PNStatus -import org.junit.Test -import java.util.UUID - -class PushPayloadHelperIntegrationTest : BaseIntegrationTest() { - @Test - fun testIntercept() { - val expectedChannel = UUID.randomUUID().toString() - - val payload = Gson().fromJson(json, JsonObject::class.java) - - pubnub.addListener( - object : SubscribeCallback() { - override fun status( - pubnub: PubNub, - pnStatus: PNStatus, - ) { - pubnub.publish( - channel = expectedChannel, - message = payload, - ).sync() - } - }, - ) - - pubnub.subscribe( - channels = listOf(expectedChannel), - withPresence = true, - ) - - wait() - } - - private val json = - """ - { - "match": { - "tournament": "Barclay's Premier League", - "date": "2018-04-05 13:30:45", - "venue": "Anfield Road", - "title": "Goal! 90 min", - "summary": "Liverpool - Chelsea 2:1" - }, - "pn_gcm": { - "data": { - "title": "Goal! 90 min", - "summary": "Liverpool - Chelsea 2:1" - } - }, - "pn_apns": { - "aps": { - "title": "Goal! 90 min", - "summary": "Liverpool - Chelsea 2:1" - } - }, - "pn_mpns": { - "title": "Goal! 90 min", - "summary": "Liverpool - Chelsea 2:1" - } - } - """.trimIndent() -} diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/test/Extensions.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/test/Extensions.kt index c6aab72a48..15d9ffc304 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/test/Extensions.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/test/Extensions.kt @@ -45,6 +45,7 @@ fun AtomicBoolean.listen(function: () -> Boolean): AtomicBoolean { fun RemoteAction.asyncRetry(function: (result: Result) -> Unit) { val hits = AtomicInteger(0) + var lastException: Throwable? = null val block = { hits.incrementAndGet() @@ -55,7 +56,15 @@ fun RemoteAction.asyncRetry(function: (result: Result) try { function.invoke(result) success.set(true) + } catch (e: AssertionError) { + // Test logic errors - fail immediately with clear context + throw AssertionError("Assertion failed on attempt ${hits.get()}: ${e.message}", e) + } catch (e: org.opentest4j.AssertionFailedError) { + // JUnit 5 assertion failures - fail immediately + throw e } catch (e: Throwable) { + // Environmental failures - save for potential retry + lastException = e success.set(false) } latch.countDown() @@ -127,6 +136,13 @@ fun PubNub.subscribeToBlocking(vararg channels: String) { Thread.sleep(2000) } +fun PubNub.subscribeNonBlocking(vararg channels: String) { + this.subscribe( + channels = listOf(*channels), + withPresence = true, + ) +} + fun PubNub.unsubscribeFromBlocking(vararg channels: String) { unsubscribe( channels = listOf(*channels), diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/resources/logback.xml b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/resources/logback.xml index 90b4f71928..a826781b3d 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/resources/logback.xml +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/resources/logback.xml @@ -7,7 +7,7 @@ - + diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/PubNubImpl.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/PubNubImpl.kt index b48fa00d7a..7bcf2e5c86 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/PubNubImpl.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/PubNubImpl.kt @@ -578,6 +578,8 @@ open class PubNubImpl( channelGroups: List, includeState: Boolean, includeUUIDs: Boolean, + limit: Int, + offset: Int?, ): HereNow { return HereNowEndpoint( pubnub = this, @@ -585,6 +587,8 @@ open class PubNubImpl( channelGroups = channelGroups, includeState = includeState, includeUUIDs = includeUUIDs, + limit = limit, + offset = offset, ) } diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/endpoints/presence/HereNowEndpoint.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/endpoints/presence/HereNowEndpoint.kt index b23ce7d0b2..5935306d9f 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/endpoints/presence/HereNowEndpoint.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/main/kotlin/com/pubnub/internal/endpoints/presence/HereNowEndpoint.kt @@ -18,6 +18,8 @@ import com.pubnub.internal.toCsv import retrofit2.Call import retrofit2.Response +private const val MAX_CHANNEL_OCCUPANTS_LIMIT = 1000 + /** * @see [PubNubImpl.hereNow] */ @@ -27,6 +29,8 @@ class HereNowEndpoint internal constructor( override val channelGroups: List = emptyList(), override val includeState: Boolean = false, override val includeUUIDs: Boolean = true, + override val limit: Int = MAX_CHANNEL_OCCUPANTS_LIMIT, + override val offset: Int? = null, ) : EndpointCore, PNHereNowResult>(pubnub), HereNow { private val log: PNLogger = LoggerManager.instance.getLogger(pubnub.logConfig, this::class.java) @@ -45,7 +49,9 @@ class HereNowEndpoint internal constructor( "channelGroups" to channelGroups, "includeState" to includeState, "includeUUIDs" to includeUUIDs, - "isGlobalHereNow" to isGlobalHereNow() + "limit" to limit, + "offset" to (offset?.toString() ?: "null"), + "isGlobalHereNow" to isGlobalHereNow(), ), operation = this::class.simpleName ), @@ -71,7 +77,7 @@ class HereNowEndpoint internal constructor( override fun createResponse(input: Response>): PNHereNowResult { return if (isGlobalHereNow() || (channels.size > 1 || channelGroups.isNotEmpty())) { - parseMultipleChannelResponse(input.body()?.payload!!) + parseMultipleChannelResponse(input.body()!!.payload!!) } else { parseSingleChannelResponse(input.body()!!) } @@ -82,48 +88,63 @@ class HereNowEndpoint internal constructor( override fun getEndpointGroupName(): RetryableEndpointGroup = RetryableEndpointGroup.PRESENCE private fun parseSingleChannelResponse(input: Envelope): PNHereNowResult { - val pnHereNowResult = - PNHereNowResult( - totalChannels = 1, - totalOccupancy = input.occupancy, - ) + val occupants = if (includeUUIDs) { + when { + input.uuids != null -> prepareOccupantData(input.uuids) + limit == 0 -> emptyList() // Server omits uuids field when limit=0 + else -> prepareOccupantData(input.uuids!!) + } + } else { + emptyList() + } - val pnHereNowChannelData = - PNHereNowChannelData( - channelName = channels[0], - occupancy = input.occupancy, - ) + val pnHereNowResult = PNHereNowResult( + totalChannels = 1, + totalOccupancy = input.occupancy, + ) - if (includeUUIDs) { - pnHereNowChannelData.occupants = prepareOccupantData(input.uuids!!) - pnHereNowResult.channels[channels[0]] = pnHereNowChannelData - } + // When includeUUIDs = false, occupants list will be empty but channel data is still present + val pnHereNowChannelData = PNHereNowChannelData( + channelName = channels[0], + occupancy = input.occupancy, + occupants = occupants + ) + pnHereNowResult.channels[channels[0]] = pnHereNowChannelData return pnHereNowResult } private fun parseMultipleChannelResponse(input: JsonElement): PNHereNowResult { - val pnHereNowResult = - PNHereNowResult( - totalChannels = pubnub.mapper.elementToInt(input, "total_channels"), - totalOccupancy = pubnub.mapper.elementToInt(input, "total_occupancy"), - ) - - val it = pubnub.mapper.getObjectIterator(input, "channels") - - while (it.hasNext()) { - val entry = it.next() - val pnHereNowChannelData = - PNHereNowChannelData( - channelName = entry.key, - occupancy = pubnub.mapper.elementToInt(entry.value, "occupancy"), - ) - if (includeUUIDs) { - pnHereNowChannelData.occupants = prepareOccupantData(pubnub.mapper.getField(entry.value, "uuids")!!) + val channels = pubnub.mapper.getObjectIterator(input, "channels") + + val channelsMap = mutableMapOf() + while (channels.hasNext()) { + val entry = channels.next() + val uuidsField = pubnub.mapper.getField(entry.value, "uuids") + val occupants = if (includeUUIDs) { + when { + uuidsField != null -> prepareOccupantData(uuidsField) + limit == 0 -> emptyList() // Server omits uuids field when limit=0 + else -> prepareOccupantData(uuidsField!!) + } + } else { + emptyList() } - pnHereNowResult.channels[entry.key] = pnHereNowChannelData + + val pnHereNowChannelData = PNHereNowChannelData( + channelName = entry.key, + occupancy = pubnub.mapper.elementToInt(entry.value, "occupancy"), + occupants = occupants + ) + channelsMap[entry.key] = pnHereNowChannelData } + val pnHereNowResult = PNHereNowResult( + totalChannels = pubnub.mapper.elementToInt(input, "total_channels"), + totalOccupancy = pubnub.mapper.elementToInt(input, "total_occupancy"), + channels = channelsMap, + ) + return pnHereNowResult } @@ -160,5 +181,7 @@ class HereNowEndpoint internal constructor( if (channelGroups.isNotEmpty()) { queryParams["channel-group"] = channelGroups.toCsv() } + queryParams["limit"] = limit.toString() + offset?.let { queryParams["offset"] = it.toString() } } } diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/PubNubImplTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/PubNubImplTest.kt index 94475cb107..c9c420a8ef 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/PubNubImplTest.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/PubNubImplTest.kt @@ -56,7 +56,7 @@ class PubNubImplTest : BaseTest() { fun getVersionAndTimeStamp() { val version = PubNubImpl.SDK_VERSION val timeStamp = PubNubImpl.timestamp() - assertEquals("10.6.0", version) + assertEquals("11.0.0", version) assertTrue(timeStamp > 0) } diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/AddChannelsToPushTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/AddChannelsToPushTest.kt index 084ad5dc4e..8706fcd721 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/AddChannelsToPushTest.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/AddChannelsToPushTest.kt @@ -51,28 +51,7 @@ class AddChannelsToPushTest : BaseTest() { val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) assertEquals(1, requests.size) assertEquals("ch1,ch2,ch3", requests[0].queryParameter("add").firstValue()) - assertEquals("gcm", requests[0].queryParameter("type").firstValue()) - assertFalse(requests[0].queryParameter("environment").isPresent) - assertFalse(requests[0].queryParameter("topic").isPresent) - } - - @Test - fun testAddMicrosoftSuccessSync() { - WireMock.stubFor( - WireMock.get(WireMock.urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) - .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")), - ) - - pubnub.addPushNotificationsOnChannels( - deviceId = "niceDevice", - pushType = PNPushType.MPNS, - channels = listOf("ch1", "ch2", "ch3"), - ).sync() - - val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) - assertEquals(1, requests.size) - assertEquals("ch1,ch2,ch3", requests[0].queryParameter("add").firstValue()) - assertEquals("mpns", requests[0].queryParameter("type").firstValue()) + assertEquals("fcm", requests[0].queryParameter("type").firstValue()) assertFalse(requests[0].queryParameter("environment").isPresent) assertFalse(requests[0].queryParameter("topic").isPresent) } diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/ListPushProvisionsTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/ListPushProvisionsTest.kt index c8bce61f59..7f7e90af06 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/ListPushProvisionsTest.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/ListPushProvisionsTest.kt @@ -64,30 +64,7 @@ class ListPushProvisionsTest : BaseTest() { val requests = findAll(getRequestedFor(urlMatching("/.*"))) assertEquals(1, requests.size) - assertEquals("gcm", requests[0].queryParameter("type").firstValue()) - assertFalse(requests[0].queryParameter("environment").isPresent) - assertFalse(requests[0].queryParameter("topic").isPresent) - } - - @Test - fun testMicrosoftSuccessSync() { - stubFor( - get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) - .willReturn(aResponse().withBody("""["ch1", "ch2", "ch3"]""")), - ) - - val response = - pubnub.auditPushChannelProvisions( - deviceId = "niceDevice", - pushType = PNPushType.MPNS, - topic = "irrelevant", - ).sync() - - assertEquals(listOf("ch1", "ch2", "ch3"), response.channels) - - val requests = findAll(getRequestedFor(urlMatching("/.*"))) - assertEquals(1, requests.size) - assertEquals("mpns", requests[0].queryParameter("type").firstValue()) + assertEquals("fcm", requests[0].queryParameter("type").firstValue()) assertFalse(requests[0].queryParameter("environment").isPresent) assertFalse(requests[0].queryParameter("topic").isPresent) } diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/PushPayloadHelperHelperTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/PushPayloadHelperHelperTest.kt index f08f836429..b4a22130ba 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/PushPayloadHelperHelperTest.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/PushPayloadHelperHelperTest.kt @@ -19,7 +19,6 @@ import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper.FCMPayloadV import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper.FCMPayloadV2.FcmOptions import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper.FCMPayloadV2.WebpushConfig import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper.FCMPayloadV2.WebpushConfig.WebpushFcmOptions -import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper.MPNSPayload import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -42,7 +41,6 @@ class PushPayloadHelperHelperTest : BaseTest() { pushPayloadHelper.commonPayload = null pushPayloadHelper.fcmPayload = null pushPayloadHelper.fcmPayloadV2 = null - pushPayloadHelper.mpnsPayload = null val map = pushPayloadHelper.build() assertTrue(map.isEmpty()) } @@ -54,7 +52,6 @@ class PushPayloadHelperHelperTest : BaseTest() { pushPayloadHelper.commonPayload = HashMap() pushPayloadHelper.fcmPayload = FCMPayload() pushPayloadHelper.fcmPayloadV2 = FCMPayloadV2() - pushPayloadHelper.mpnsPayload = MPNSPayload() val map = pushPayloadHelper.build() assertTrue(map.isEmpty()) } @@ -611,113 +608,4 @@ class PushPayloadHelperHelperTest : BaseTest() { assertNotNull(pnFcmMap["key_3"]) assertNotNull(pnFcmMap["key_4"]) } - - @Test - fun testMicrosoft_Missing() { - val pushPayloadHelper = PushPayloadHelper() - - val mpnsPayload = - MPNSPayload().apply { - backContent = "Back Content" - backTitle = "Back Title" - count = 1 - title = "Title" - type = "Type" - custom = - mapOf( - "a" to "a", - "b" to 1, - "c" to "", - ) - } - pushPayloadHelper.mpnsPayload = mpnsPayload - val map = pushPayloadHelper.build() - - val pnMpnsMap = map["pn_mpns"] as HashMap<*, *> - assertNotNull(pnMpnsMap) - assertEquals(pnMpnsMap["back_content"], "Back Content") - assertEquals(pnMpnsMap["back_title"], "Back Title") - assertEquals(pnMpnsMap["count"], 1) - assertEquals(pnMpnsMap["title"], "Title") - assertEquals(pnMpnsMap["type"], "Type") - assertEquals(pnMpnsMap["a"], "a") - assertEquals(pnMpnsMap["b"], 1) - assertEquals(pnMpnsMap["c"], "") - } - - @Test - fun testMicrosoft_Valid() { - val pushPayloadHelper = PushPayloadHelper() - pushPayloadHelper.mpnsPayload = - MPNSPayload().apply { - backContent = "Back Content" - backTitle = "Back Title" - count = 1 - title = "Title" - type = "Type" - custom = - mapOf( - "a" to "a", - "b" to 1, - "c" to "", - ) - } - - val map = pushPayloadHelper.build() - val pnMpnsMap = map["pn_mpns"] as Map<*, *> - - assertNotNull(pnMpnsMap) - assertEquals(pnMpnsMap["back_content"], "Back Content") - assertEquals(pnMpnsMap["back_title"], "Back Title") - assertEquals(pnMpnsMap["count"], 1) - assertEquals(pnMpnsMap["title"], "Title") - assertEquals(pnMpnsMap["type"], "Type") - assertEquals(pnMpnsMap["a"], "a") - assertEquals(pnMpnsMap["b"], 1) - assertEquals(pnMpnsMap["c"], "") - assertEquals(pnMpnsMap["d"], null) - } - - @Test - fun testMicrosoft_Empty() { - val pushPayloadHelper = PushPayloadHelper() - val mpnsPayload = MPNSPayload() - pushPayloadHelper.mpnsPayload = mpnsPayload - val map = pushPayloadHelper.build() - val pnMpnsMap = map["pn_mpns"] - assertNull(pnMpnsMap) - } - - @Test - fun testMicrosoft_Custom() { - val pushPayloadHelper = PushPayloadHelper() - pushPayloadHelper.mpnsPayload = - MPNSPayload().apply { - backContent = "" - backTitle = "Back Title" - count = 1 - title = null - type = "Type" - custom = - mapOf( - "a" to "a", - "b" to 1, - "c" to "", - ) - } - - val map = pushPayloadHelper.build() - - val pnMpnsMap = map["pn_mpns"] as Map<*, *> - assertNotNull(pnMpnsMap) - assertEquals("", pnMpnsMap["back_content"]) - assertEquals("Back Title", pnMpnsMap["back_title"]) - assertEquals(1, pnMpnsMap["count"]) - assertFalse(pnMpnsMap.containsKey("title")) - assertEquals("Type", pnMpnsMap["type"]) - assertEquals("a", pnMpnsMap["a"]) - assertEquals(1, pnMpnsMap["b"]) - assertEquals("", pnMpnsMap["c"]) - assertFalse(pnMpnsMap.containsKey("d")) - } } diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveAllPushChannelsForDeviceTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveAllPushChannelsForDeviceTest.kt index e0d562f6fc..1d63688812 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveAllPushChannelsForDeviceTest.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveAllPushChannelsForDeviceTest.kt @@ -53,26 +53,7 @@ class RemoveAllPushChannelsForDeviceTest : BaseTest() { val requests = findAll(getRequestedFor(urlMatching("/.*"))) assertEquals(1, requests.size) - assertEquals("gcm", requests[0].queryParameter("type").firstValue()) - assertFalse(requests[0].queryParameter("environment").isPresent) - assertFalse(requests[0].queryParameter("topic").isPresent) - } - - @Test - fun testMicrosoftSuccessSyncRemoveAll() { - stubFor( - get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice/remove")) - .willReturn(aResponse().withBody("[1, \"Modified Channels\"]")), - ) - - pubnub.removeAllPushNotificationsFromDeviceWithPushToken( - deviceId = "niceDevice", - pushType = PNPushType.MPNS, - ).sync() - - val requests = findAll(getRequestedFor(urlMatching("/.*"))) - assertEquals(1, requests.size) - assertEquals("mpns", requests[0].queryParameter("type").firstValue()) + assertEquals("fcm", requests[0].queryParameter("type").firstValue()) assertFalse(requests[0].queryParameter("environment").isPresent) assertFalse(requests[0].queryParameter("topic").isPresent) } diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveChannelsFromPushTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveChannelsFromPushTest.kt index a2a09663c1..fa5866f32b 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveChannelsFromPushTest.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveChannelsFromPushTest.kt @@ -50,28 +50,7 @@ class RemoveChannelsFromPushTest : BaseTest() { val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) assertEquals(1, requests.size) - assertEquals("gcm", requests[0].queryParameter("type").firstValue()) - assertEquals("chr1,chr2,chr3", requests[0].queryParameter("remove").firstValue()) - assertFalse(requests[0].queryParameter("environment").isPresent) - assertFalse(requests[0].queryParameter("topic").isPresent) - } - - @Test - fun testRemoveMicrosoftSuccessSync() { - WireMock.stubFor( - WireMock.get(WireMock.urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) - .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")), - ) - - pubnub.removePushNotificationsFromChannels( - deviceId = "niceDevice", - pushType = PNPushType.MPNS, - channels = listOf("chr1", "chr2", "chr3"), - ).sync() - - val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) - assertEquals(1, requests.size) - assertEquals("mpns", requests[0].queryParameter("type").firstValue()) + assertEquals("fcm", requests[0].queryParameter("type").firstValue()) assertEquals("chr1,chr2,chr3", requests[0].queryParameter("remove").firstValue()) assertFalse(requests[0].queryParameter("environment").isPresent) assertFalse(requests[0].queryParameter("topic").isPresent) diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/endpoints/presence/HereNowEndpointTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/endpoints/presence/HereNowEndpointTest.kt new file mode 100644 index 0000000000..fa4f29156f --- /dev/null +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/endpoints/presence/HereNowEndpointTest.kt @@ -0,0 +1,71 @@ +package com.pubnub.internal.endpoints.presence + +import com.pubnub.api.legacy.BaseTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test + +class HereNowEndpointTest : BaseTest() { + @Test + fun testPubNubHereNowWithPaginationParameters() { + // Test the public API with new pagination parameters + val hereNow = pubnub.hereNow( + channels = listOf("test-channel"), + channelGroups = emptyList(), + includeState = false, + includeUUIDs = true, + limit = 50, + offset = 100 + ) + + assertNotNull(hereNow) + assertEquals(50, (hereNow as HereNowEndpoint).limit) + assertEquals(100, hereNow.offset) + } + + @Test + fun testPubNubHereNowWithDefaultParameters() { + // Test that default parameters still work (backward compatibility) + val hereNow = pubnub.hereNow( + channels = listOf("test-channel"), + channelGroups = emptyList(), + includeState = false, + includeUUIDs = true + ) + + assertNotNull(hereNow) + assertEquals(1000, (hereNow as HereNowEndpoint).limit) // Default limit is 1000 + assertNull(hereNow.offset) + } + + @Test + fun testHereNowAcceptsDefaultLimitValue() { + // Test maximum valid limit value + val hereNow = pubnub.hereNow( + channels = listOf("test-channel"), + includeUUIDs = true, + limit = 1000 + ) + assertNotNull(hereNow) + assertEquals(1000, (hereNow as HereNowEndpoint).limit) + } + + @Test + fun testHereNowAcceptsLimitBeyondAdvisedMaximum() { + val hereNow = pubnub.hereNow(channels = listOf("test"), limit = 5000) + assertEquals(5000, (hereNow as HereNowEndpoint).limit) // Passes to server for validation + } + + @Test + fun testHereNowStartFromLargeValue() { + // Test large valid offset value + val hereNow = pubnub.hereNow( + channels = listOf("test-channel"), + includeUUIDs = true, + offset = 1000000 + ) + assertNotNull(hereNow) + assertEquals(1000000, (hereNow as HereNowEndpoint).offset) + } +} diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/add/AddChannelsToPushV1TestSuite.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/add/AddChannelsToPushV1TestSuite.kt index 80b091f328..53009f9ef5 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/add/AddChannelsToPushV1TestSuite.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/add/AddChannelsToPushV1TestSuite.kt @@ -38,7 +38,7 @@ class AddChannelsToPushV1TestSuite : override fun mappingBuilder() = get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/12345")) - .withQueryParam("type", equalTo("gcm")) + .withQueryParam("type", equalTo("fcm")) .withQueryParam("add", equalTo("ch1,ch2")) .withQueryParam("environment", absent()) .withQueryParam("topic", absent()) diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/list/ListPushProvisionsV1TestSuite.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/list/ListPushProvisionsV1TestSuite.kt index 7701e1b1f8..532290f830 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/list/ListPushProvisionsV1TestSuite.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/list/ListPushProvisionsV1TestSuite.kt @@ -42,7 +42,7 @@ class ListPushProvisionsV1TestSuite : override fun mappingBuilder() = get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/12345")) - .withQueryParam("type", equalTo("gcm")) + .withQueryParam("type", equalTo("fcm")) .withQueryParam("environment", absent()) .withQueryParam("topic", absent()) diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/remove/RemoveAllFromPushV1TestSuite.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/remove/RemoveAllFromPushV1TestSuite.kt index 93b6b9b8bc..3d40b2d651 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/remove/RemoveAllFromPushV1TestSuite.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/remove/RemoveAllFromPushV1TestSuite.kt @@ -33,7 +33,7 @@ class RemoveAllFromPushV1TestSuite : override fun mappingBuilder() = get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/12345/remove")) - .withQueryParam("type", equalTo("gcm")) + .withQueryParam("type", equalTo("fcm")) .withQueryParam("environment", absent()) .withQueryParam("topic", absent()) diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/remove/RemoveChannelsFromPushV1TestSuite.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/remove/RemoveChannelsFromPushV1TestSuite.kt index ab9eb582ed..e53e1606da 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/remove/RemoveChannelsFromPushV1TestSuite.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/internal/suite/push/remove/RemoveChannelsFromPushV1TestSuite.kt @@ -34,7 +34,7 @@ class RemoveChannelsFromPushV1TestSuite : override fun mappingBuilder() = get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/12345")) - .withQueryParam("type", equalTo("gcm")) + .withQueryParam("type", equalTo("fcm")) .withQueryParam("remove", equalTo("ch1,ch2")) .withQueryParam("environment", absent()) .withQueryParam("topic", absent())