From 672393d05e051198ed7663cf82075ac8e3135a4c Mon Sep 17 00:00:00 2001 From: marcin-cebo <102806110+marcin-cebo@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:46:31 +0100 Subject: [PATCH] Added tests for access manager. (#164) --- js-chat/tests/access-manager.test.ts | 106 ++++++++++++++++++ js-chat/tests/utils.ts | 86 ++++++++++---- .../pubnub/integration/AccessManagerTest.kt | 51 ++++++++- .../compubnub/chat/ChatIntegrationTest.kt | 85 +++++++++++++- 4 files changed, 299 insertions(+), 29 deletions(-) create mode 100644 js-chat/tests/access-manager.test.ts diff --git a/js-chat/tests/access-manager.test.ts b/js-chat/tests/access-manager.test.ts new file mode 100644 index 00000000..a0d22542 --- /dev/null +++ b/js-chat/tests/access-manager.test.ts @@ -0,0 +1,106 @@ +import { + Chat +} from "../dist-test" +import { + createChatInstance, + makeid, + sleep +} from "./utils"; + +import { jest } from "@jest/globals" + +describe("Access Manager test test", () => { + jest.retryTimes(2) + + let chatPamClient: Chat + let chatPamServer: Chat + + beforeAll(async () => { + let chatClientUserId = makeid(8) + chatPamServer = await createChatInstance( { shouldCreateNewInstance: true, clientType: 'PamServer' }) + const token = await _grantTokenForUserId(chatPamServer, chatClientUserId); + chatPamClient = await createChatInstance( { + userId: chatClientUserId , + shouldCreateNewInstance: true, + clientType: 'PamClient', + config: { + authKey: token + }, + }) + }) + + afterEach(async () => { + jest.clearAllMocks() + }) + + // test is skipped because it has 65sec sleep to wait for token expiration + test.skip("when token is updated then client can use API", async () => { + const user1Id = `user1_${Date.now()}` + const userToChatWith = await chatPamServer.createUser(user1Id, { name: "User1" }) + const createDirectConversationResult = await chatPamServer.createDirectConversation( + { + user: userToChatWith, + channelData: { + name: "Quick sync on customer XYZ" + }, + membershipData: { + custom: { + purpose: "premium-support" + } + } + } + ) + let channelId = createDirectConversationResult.channel.id + let token = await _grantTokenForChannel(1, chatPamServer, channelId); + await chatPamClient.sdk.setToken(token) + + const channelRetrievedByClient = await chatPamClient.getChannel(createDirectConversationResult.channel.id); + expect(channelRetrievedByClient).toBeDefined(); + + // Verify that the fetched channel ID matches the expected channel ID + expect(channelRetrievedByClient?.id).toEqual(channelId); + + let publishResult = await channelRetrievedByClient.sendText("my first message"); + let message = await channelRetrievedByClient.getMessage(publishResult.timetoken); + await message.toggleReaction("one") + + // sleep so that token expires + await sleep(65000) + token = await _grantTokenForChannel(1, chatPamServer, channelId); + await chatPamClient.sdk.setToken(token) + + await message.toggleReaction("two"); + await chatPamClient.getChannel(channelRetrievedByClient?.id); + + await chatPamServer.deleteChannel(channelId) + }, 100000); // this long timeout is needed so we can wait 65sec for token to expire + + async function _grantTokenForUserId(chatPamServer, chatClientUserId) { + return chatPamServer.sdk.grantToken({ + ttl: 10, + resources: { + uuids: { + [chatClientUserId]: { + get: true, + update: true + } + } + } + }); + } + + async function _grantTokenForChannel(ttl, chatPamServer, channelId) { + return chatPamServer.sdk.grantToken({ + ttl: ttl, + resources: { + channels: { + [channelId]: { + read: true, + write: true, + get: true, // this is important + } + } + } + }); + } +}) diff --git a/js-chat/tests/utils.ts b/js-chat/tests/utils.ts index 2328f04f..2524920e 100644 --- a/js-chat/tests/utils.ts +++ b/js-chat/tests/utils.ts @@ -25,43 +25,83 @@ export function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) } -export async function createChatInstance( - options: { - userId?: string - shouldCreateNewInstance?: boolean - config?: Partial & PubNub.PubnubConfig - } = {} -) { +type ClientType = 'PamClient' | 'PamServer' | 'NoPam'; + +const createChat = async ( + userId: string, + config?: Partial & PubNub.PubnubConfig, + clientType?: ClientType, + +): Promise => { + const keysetError = ` ####################################################### # Could not read the PubNub keyset from the .env file # ####################################################### ` - if (!process.env.PUBLISH_KEY || !process.env.SUBSCRIBE_KEY || !process.env.USER_ID) + // Determine keys based on clientType + let publishKey: string | undefined; + let subscribeKey: string | undefined; + let secretKey: string | undefined; + + switch (clientType) { + case 'PamClient': + publishKey = process.env.PAM_PUBLISH_KEY; + subscribeKey = process.env.PAM_SUBSCRIBE_KEY; + break; + + case 'PamServer': + publishKey = process.env.PAM_PUBLISH_KEY; + subscribeKey = process.env.PAM_SUBSCRIBE_KEY; + secretKey = process.env.PAM_SECRET_KEY; + break; + + case 'NoPam': + default: + publishKey = process.env.PUBLISH_KEY; + subscribeKey = process.env.SUBSCRIBE_KEY; + break; + } + + // Validate required keys + if (!publishKey || !subscribeKey || (clientType === 'PamServer' && !secretKey)) { throw keysetError + } + // Build the chat configuration + const chatConfig: Partial & PubNub.PubnubConfig = { + publishKey, + subscribeKey, + userId, + ...config, + }; + + // Include secretKey only if clientType is 'PamServer' + if (clientType === 'PamServer' && secretKey) { + chatConfig.secretKey = secretKey; + } + + return Chat.init(chatConfig); +}; + +export async function createChatInstance( + options: { + userId?: string + shouldCreateNewInstance?: boolean + config?: Partial & PubNub.PubnubConfig + clientType?: ClientType + } = {} +) { if (options.shouldCreateNewInstance) { - return await Chat.init({ - publishKey: process.env.PUBLISH_KEY, - subscribeKey: process.env.SUBSCRIBE_KEY, - userId: options.userId || process.env.USER_ID, -// logVerbosity: true, - ...options.config, - }) + return await createChat(options.userId || process.env.USER_ID!, options.config, options.clientType); } if (!chat) { - chat = await Chat.init({ - publishKey: process.env.PUBLISH_KEY, - subscribeKey: process.env.SUBSCRIBE_KEY, - userId: options.userId || process.env.USER_ID, -// logVerbosity: true, - ...options.config, - }) + chat = await createChat(options.userId || process.env.USER_ID!, options.config, options.clientType); } - return chat + return chat; } export function createRandomChannel(prefix?: string) { diff --git a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/AccessManagerTest.kt b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/AccessManagerTest.kt index 45f15fe0..e339d540 100644 --- a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/AccessManagerTest.kt +++ b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/AccessManagerTest.kt @@ -3,6 +3,7 @@ package com.pubnub.integration import com.pubnub.api.PubNubException import com.pubnub.api.models.consumer.access_manager.v3.ChannelGrant import com.pubnub.api.models.consumer.access_manager.v3.UUIDGrant +import com.pubnub.chat.Chat import com.pubnub.chat.Event import com.pubnub.chat.internal.message.MessageImpl import com.pubnub.chat.listenForEvents @@ -10,9 +11,11 @@ import com.pubnub.chat.types.EventContent import com.pubnub.internal.PLATFORM import com.pubnub.test.await import kotlinx.coroutines.test.runTest +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.time.Duration.Companion.minutes class AccessManagerTest : BaseChatIntegrationTest() { @Test @@ -34,7 +37,15 @@ class AccessManagerTest : BaseChatIntegrationTest() { chatPamServer.createChannel(id = channelId).await() val token = chatPamServer.pubNub.grantToken( ttl = 1, - channels = listOf(ChannelGrant.name(get = true, name = channelId, read = true, write = true, manage = true)) // get = true + channels = listOf( + ChannelGrant.name( + get = true, + name = channelId, + read = true, + write = true, + manage = true + ) + ) // get = true ).await().token // client uses token generated by server chatPamClient.pubNub.setToken(token) @@ -47,6 +58,37 @@ class AccessManagerTest : BaseChatIntegrationTest() { chatPamServer.deleteChannel(id = channelId).await() } + @Ignore // this test has 65 sec delay to wait for token to expire. To run it on JS extend timeout in Mocha to 70s. + @Test + fun `when token is updated then client can use API`() = runTest(timeout = 2.minutes) { + if (PLATFORM == "iOS") { + return@runTest + } + // getToken from server + val channelId = channelPam.id + chatPamServer.createChannel(id = channelId).await() + // todo extract to the function ? + val token = generateToken(chatPamServer, channelId, 1) + chatPamClient.pubNub.setToken(token) + + val channel = chatPamClient.getChannel(channelId).await()!! + val actualChannelId = channel.id + assertEquals(channelId, actualChannelId) + + val publishResult = channel.sendText("my first message").await() + val message = channel.getMessage(publishResult.timetoken).await()!! + message.toggleReaction("one").await() + + delayInMillis(65000) + val token2 = generateToken(chatPamServer, channelId, 1) + chatPamClient.pubNub.setToken(token2) + + message.toggleReaction("three").await() + chatPamClient.getChannel(channelId).await()?.id + + chatPamServer.deleteChannel(id = channelId).await() + } + @Test fun setLastReadMessageTimetoken_should_send_Receipt_event_when_has_token() = runTest { if (PLATFORM == "iOS") { @@ -100,4 +142,11 @@ class AccessManagerTest : BaseChatIntegrationTest() { chatPamServer.deleteChannel(channelId).await() } + + private suspend fun generateToken(chat: Chat, channelId: String, ttl: Int): String { + return chat.pubNub.grantToken( + ttl = ttl, + channels = listOf(ChannelGrant.name(get = true, name = channelId, read = true, write = true, manage = true)) + ).await().token + } } diff --git a/src/jvmTest/kotlin/compubnub/chat/ChatIntegrationTest.kt b/src/jvmTest/kotlin/compubnub/chat/ChatIntegrationTest.kt index 43ad46f6..06363ddf 100644 --- a/src/jvmTest/kotlin/compubnub/chat/ChatIntegrationTest.kt +++ b/src/jvmTest/kotlin/compubnub/chat/ChatIntegrationTest.kt @@ -7,6 +7,7 @@ import com.pubnub.api.asMap import com.pubnub.api.asString import com.pubnub.api.enums.PNLogVerbosity import com.pubnub.api.models.consumer.access_manager.v3.ChannelGrant +import com.pubnub.api.models.consumer.access_manager.v3.PNGrantTokenResult import com.pubnub.api.models.consumer.access_manager.v3.UUIDGrant import com.pubnub.api.v2.PNConfiguration import com.pubnub.api.v2.callbacks.Result @@ -23,25 +24,99 @@ import com.pubnub.test.await import com.pubnub.test.randomString import kotlinx.coroutines.test.runTest import org.junit.Assert.assertTrue +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull import kotlin.time.Duration.Companion.seconds class ChatIntegrationTest : BaseIntegrationTest() { @Test fun canInitializeChatWithLogLevel() { - val chatConfig = ChatConfiguration(logLevel = LogLevel.OFF) + val countDownLatch = CountDownLatch(1) + val chatConfig = ChatConfiguration(logLevel = LogLevel.VERBOSE) val pnConfiguration = - PNConfiguration.builder(userId = UserId("myUserId"), subscribeKey = "mySubscribeKey").build() + PNConfiguration.builder(userId = UserId("myUserId"), subscribeKey = Keys.subKey) { + logVerbosity = PNLogVerbosity.BODY + }.build() Chat.init(chatConfig, pnConfiguration).async { result: Result -> result.onSuccess { chat: Chat -> - println("Chat successfully initialized having logLevel: ${chatConfig.logLevel}") + countDownLatch.countDown() }.onFailure { exception: PubNubException -> - println("Exception initialising chat: ${exception.message}") + throw Exception("Exception initialising chat: ${exception.message}") } } + + assertTrue(countDownLatch.await(3, TimeUnit.SECONDS)) + } + + @Test + fun shouldThrowException_when_initializingChatWithoutSecretKeyAndWithPamEnabled_and_noToken() { + val chatConfig = ChatConfiguration(logLevel = LogLevel.VERBOSE) + val pnConfiguration = + PNConfiguration.builder(userId = UserId("myUserId"), subscribeKey = Keys.pamSubKey) { + logVerbosity = PNLogVerbosity.BODY + }.build() + var capturedException: Exception? = null + val latch = CountDownLatch(1) + + Chat.init(chatConfig, pnConfiguration).async { result: Result -> + result.onSuccess { chat: Chat -> + }.onFailure { exception: PubNubException -> + capturedException = exception + latch.countDown() + } + } + + latch.await(3, TimeUnit.SECONDS) + val exceptionMessage = capturedException?.message + assertNotNull(exceptionMessage, "Exception message should not be null") + assertTrue(exceptionMessage.contains("\"status\": 403")) + assertTrue(exceptionMessage.contains("\"message\": \"Forbidden\"")) + } + + @Test + fun shouldInitializingChatWithoutSecretKeyWithPamEnabled_when_tokenProvided() { + val grantTokenLatch = CountDownLatch(1) + val initLatchChatServer = CountDownLatch(1) + val initLatchChatClient = CountDownLatch(1) + val chatClientUserId = randomString() + val chatConfig = ChatConfiguration(logLevel = LogLevel.VERBOSE) + var chatPamServer: Chat? = null + Chat.init(chatConfig, configPamServer).async { result: Result -> + result.onSuccess { chat: Chat -> + chatPamServer = chat + initLatchChatServer.countDown() + } + } + + initLatchChatServer.await(3, TimeUnit.SECONDS) + var token: String? = null + chatPamServer?.pubNub?.grantToken(ttl = 1, uuids = listOf(UUIDGrant.id(id = chatClientUserId, get = true, update = true)))?.async { + result: Result -> + result.onSuccess { grantTokenResult: PNGrantTokenResult -> + token = grantTokenResult.token + grantTokenLatch.countDown() + } + } + + grantTokenLatch.await(3, TimeUnit.SECONDS) + val pnConfiguration = + PNConfiguration.builder(userId = UserId(chatClientUserId), subscribeKey = Keys.pamSubKey) { + logVerbosity = PNLogVerbosity.BODY + authToken = token + }.build() + + Chat.init(chatConfig, pnConfiguration).async { result: Result -> + result.onSuccess { chat: Chat -> + initLatchChatClient.countDown() + } + } + + assertTrue(initLatchChatClient.await(3, TimeUnit.SECONDS)) } @Test @@ -151,7 +226,7 @@ class ChatIntegrationTest : BaseIntegrationTest() { result.onSuccess { createdChat: Chat -> chat = createdChat }.onFailure { exception: PubNubException -> - println("Exception initialising chat: ${exception.message}") + throw Exception("Exception initialising chat: ${exception.message}") } } }