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 870e30f833..a32f830d94 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 @@ -738,10 +738,18 @@ actual interface PubNub : StatusEmitter, EventEmitter { * The count returned is the number of messages in history with a timetoken value greater * than the passed value in the [MessageCounts.channelsTimetoken] parameter. * + * **Important:** The timetoken represents an exclusive boundary. Messages with timetokens + * greater than (but not equal to) the specified timetoken are counted. To count messages + * from a specific message onwards, you typically need to subtract 1 from the message's + * timetoken. + * * @param channels Channels to fetch the message count from. - * @param channelsTimetoken List of timetokens, in order of the channels list. - * Specify a single timetoken to apply it to all channels. - * Otherwise, the list of timetokens must be the same length as the list of channels. + * @param channelsTimetoken List of timetokens representing exclusive boundaries for message counting. + * Each timetoken corresponds to a channel in the same order. + * - **Single timetoken**: Applied to all channels (list with one element) + * - **Multiple timetokens**: Must match the number of channels exactly + * - **Exclusive boundary**: Only messages with timetokens > specified value are counted + * - **Common pattern**: Use `(messageTimetoken - 1)` to count from a specific message onwards */ actual fun messageCounts( channels: List, 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 7e781d511a..e855bd4fe3 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 @@ -19,7 +19,7 @@ enum class PubNubError(private val code: Int, val message: String) { SECRET_KEY_MISSING( 114, - "ULS configuration failed. Secret Key not configured", + "Secret Key not configured", ), JSON_ERROR( diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/GroupManagementIntegrationTests.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/GroupManagementIntegrationTests.kt index 3880547749..838cee9a9f 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/GroupManagementIntegrationTests.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/GroupManagementIntegrationTests.kt @@ -102,12 +102,23 @@ class GroupManagementIntegrationTests : BaseIntegrationTest() { channelGroup = expectedGroup, channels = listOf(expectedChannel1, expectedChannel2, expectedChannel3), ).await { result -> - assertFalse(result.isFailure) // TODO is this part of the result? if not then there's nothing to assert on -// assertEquals(1, status.affectedChannelGroups.size) -// assertEquals(3, status.affectedChannels.size) + assertFalse(result.isFailure) } } + @Test + fun testDeleteChannelGroup() { + pubnub.addChannelsToChannelGroup( + channelGroup = expectedGroup, + channels = listOf(expectedChannel1, expectedChannel2, expectedChannel3), + ).sync() + + pubnub.deleteChannelGroup(channelGroup = expectedGroup).sync() + + val listAllChannelGroupsResult = pubnub.listAllChannelGroups().sync() + assertFalse(listAllChannelGroupsResult.groups.contains(expectedGroup)) + } + private fun addChannelsToGroup() { pubnub.addChannelsToChannelGroup( channelGroup = expectedGroup, diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/HistoryIntegrationTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/HistoryIntegrationTest.kt index f89d045af6..76ffb649c3 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/HistoryIntegrationTest.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/HistoryIntegrationTest.kt @@ -25,6 +25,73 @@ import org.junit.jupiter.api.Test import java.time.Duration class HistoryIntegrationTest : BaseIntegrationTest() { + @Test + fun canGetMessageCounts() { + val channel01 = randomChannel() + val channel02 = randomChannel() + val firstPublishToChannel01Result = pubnub.publish(channel01, message = "FirstMessageChannel01").sync() + pubnub.publish(channel01, message = "SecondMessage").sync() + val firstPublishToChannel02Result = pubnub.publish(channel02, message = "FirstMessageChannel02").sync() + + // Test with multiple timetokens (one per channel) + val messagesCounts = pubnub.messageCounts( + channels = listOf(channel01, channel02), + channelsTimetoken = listOf( + firstPublishToChannel01Result.timetoken - 1, // Count from first message onwards + firstPublishToChannel02Result.timetoken - 1 // Count from first message onwards + ) + ).sync() + assertEquals(2, messagesCounts.channels[channel01]) + assertEquals(1, messagesCounts.channels[channel02]) + } + + @Test + fun canGetMessageCountsWithSingleTimetoken() { + val channel01 = randomChannel() + val channel02 = randomChannel() + val firstPublishToChannel01Result = pubnub.publish(channel01, message = "FirstMessageChannel01").sync() + pubnub.publish(channel01, message = "SecondMessage").sync() + val firstPublishToChannel02Result = pubnub.publish(channel02, message = "FirstMessageChannel02").sync() + + // Test with single timetoken applied to all channels + val messagesCounts = pubnub.messageCounts( + channels = listOf(channel01, channel02), + channelsTimetoken = listOf(firstPublishToChannel01Result.timetoken - 1) // Single timetoken for all channels + ).sync() + assertEquals(2, messagesCounts.channels[channel01]) + assertEquals(1, messagesCounts.channels[channel02]) + } + + @Test + fun canGetMessageCountsWithMultipleTimetokens() { + val channel01 = randomChannel() + val channel02 = randomChannel() + val channel03 = randomChannel() + + val firstPublishToChannel01Result = pubnub.publish(channel01, message = "FirstMessageChannel01").sync() + pubnub.publish(channel01, message = "SecondMessage").sync() + pubnub.publish(channel01, message = "ThirdMessage").sync() + + val firstPublishToChannel02Result = pubnub.publish(channel02, message = "FirstMessageChannel02").sync() + pubnub.publish(channel02, message = "SecondMessage").sync() + + val firstPublishToChannel03Result = pubnub.publish(channel03, message = "FirstMessageChannel03").sync() + + // Test with multiple timetokens, each tailored to specific channels + val messagesCounts = pubnub.messageCounts( + channels = listOf(channel01, channel02, channel03), + channelsTimetoken = listOf( + firstPublishToChannel01Result.timetoken, // Should count 2 messages (second and third) + firstPublishToChannel02Result.timetoken - 1, // Should count 2 messages (all) + firstPublishToChannel03Result.timetoken - 1 // Should count 1 message (all) + ) + ).sync() + + assertEquals(2, messagesCounts.channels[channel01]) + assertEquals(2, messagesCounts.channels[channel02]) + assertEquals(1, messagesCounts.channels[channel03]) + } + @Test fun historySingleScenario() { val channel = randomChannel() diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PublishIntegrationTests.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PublishIntegrationTests.kt index 0818938a65..1f91e5afd2 100644 --- a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PublishIntegrationTests.kt +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PublishIntegrationTests.kt @@ -8,6 +8,7 @@ import com.pubnub.api.callbacks.SubscribeCallback import com.pubnub.api.crypto.CryptoModule import com.pubnub.api.enums.PNStatusCategory import com.pubnub.api.models.consumer.PNBoundedPage +import com.pubnub.api.models.consumer.PNPublishResult import com.pubnub.api.models.consumer.PNStatus import com.pubnub.api.models.consumer.pubsub.PNMessageResult import com.pubnub.api.v2.PNConfigurationOverride @@ -37,6 +38,7 @@ import org.json.JSONArray import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test @@ -54,6 +56,15 @@ class PublishIntegrationTests : BaseIntegrationTest() { guestClient = createPubNub {} } + @Test + fun testFireMessage() { + val expectedChannel = randomChannel() + val fireResult: PNPublishResult = + pubnub.fire(channel = expectedChannel, message = generatePayload(), meta = null).sync() + + assertNotNull(fireResult.timetoken) + } + @Test fun testPublishMessage() { val expectedChannel = randomChannel() diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/endpoints/access/RevokeTokenTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/endpoints/access/RevokeTokenTest.kt new file mode 100644 index 0000000000..b1db475e61 --- /dev/null +++ b/pubnub-kotlin/pubnub-kotlin-impl/src/test/kotlin/com/pubnub/api/endpoints/access/RevokeTokenTest.kt @@ -0,0 +1,102 @@ +package com.pubnub.api.endpoints.access + +import com.pubnub.api.PubNubException +import com.pubnub.api.UserId +import com.pubnub.internal.PubNubImpl +import com.pubnub.internal.endpoints.access.RevokeTokenEndpoint +import com.pubnub.internal.managers.RetrofitManager +import com.pubnub.internal.models.server.access_manager.v3.RevokeTokenData +import com.pubnub.internal.models.server.access_manager.v3.RevokeTokenResponse +import com.pubnub.internal.services.AccessManagerService +import com.pubnub.internal.v2.PNConfigurationImpl +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import retrofit2.Call +import retrofit2.Response + +class RevokeTokenTest { + private lateinit var pubNub: PubNubImpl + + @BeforeEach + internal fun setUp() { + MockKAnnotations.init(this) + val pnConfiguration = + PNConfigurationImpl( + userId = UserId("myUserId"), + subscribeKey = "something", + secretKey = "secretKey" + ) + pubNub = spyk(PubNubImpl(configuration = pnConfiguration)) + } + + @MockK + private lateinit var revokeTokenEndpointMock: RevokeTokenEndpoint + + @Test + fun shouldThrowExceptionWhenSecretKeyNotProvided() { + val pnConfiguration = + PNConfigurationImpl( + userId = UserId("myUserId"), + subscribeKey = "something", + ) + pubNub = spyk(PubNubImpl(configuration = pnConfiguration)) + + val token = "test-token" + val exception = assertThrows { + pubNub.revokeToken(token).sync() + } + + assertEquals("Secret Key not configured", exception.errorMessage) + } + + @Test + fun shouldThrowExceptionWhenTokenIsBlank() { + val blankToken = "" + val exception = assertThrows { + pubNub.revokeToken(blankToken).sync() + } + + assertEquals("Token missing", exception.errorMessage) + } + + @Test + fun can_callRevokeToken() { + val expectedToken = "token_value" + val expectedSubscribeKey = "something" + + val retrofitManager = mockk(relaxed = true) + val accessManagerService = mockk(relaxed = true) + val call = mockk>() + + every { pubNub.retrofitManager } returns retrofitManager + every { retrofitManager.accessManagerService } returns accessManagerService + every { accessManagerService.revokeToken(any(), any(), any()) } returns call + every { call.execute() } returns Response.success( + RevokeTokenResponse( + 200, + RevokeTokenData("message", "token"), + "service" + ) + ) + + // Act + pubNub.revokeToken(expectedToken).sync() + + // Assert: verify the call with expected arguments + verify { + accessManagerService.revokeToken( + expectedSubscribeKey, + any(), + any() + ) + } + } +}