Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECO-5014] feat: basic sandbox setup #47

Merged
merged 1 commit into from
Nov 14, 2024
Merged

Conversation

ttypic
Copy link
Collaborator

@ttypic ttypic commented Nov 11, 2024

added basic sandbox setup for simple happy-path testing

Summary by CodeRabbit

  • New Features
    • Introduced a Sandbox class for managing Ably applications in a sandbox environment.
    • Added integration tests for the chat client to ensure functionality within the Ably framework.
  • Bug Fixes
    • Enhanced testing capabilities by adding a new dependency for Ktor client.
  • Documentation
    • Updated dependency and plugin version management for improved clarity and stability.

@ttypic ttypic requested a review from sacOO7 November 11, 2024 13:09
Copy link

coderabbitai bot commented Nov 11, 2024

Walkthrough

The changes in this pull request include the addition of a new dependency for testing in the build.gradle.kts file, the introduction of a Sandbox class and its corresponding test class SandboxTest to facilitate integration testing of the Ably chat client, and updates to the dependency management in libs.versions.toml. The new Sandbox class manages the setup and teardown of an application in the Ably sandbox environment, utilizing Ktor for HTTP requests.

Changes

File Path Change Summary
chat-android/build.gradle.kts Added new dependency: testImplementation(libs.bundles.ktor.client)
gradle/libs.versions.toml Added ktor version 3.0.1, new libraries ktor-client-core and ktor-client-cio, and a new bundle ktor-client. Updated android-kotlin plugin version to 2.0.21.
chat-android/src/test/java/com/ably/chat/Sandbox.kt Added Sandbox class with methods for setup and teardown of an application using the Ably API.
chat-android/src/test/java/com/ably/chat/SandboxTest.kt Added SandboxTest class with integration tests for the chat client, including setup and teardown methods.

Assessment against linked issues

Objective Addressed Explanation
Implement test sandbox API (ECO-5014)

Possibly related issues

Possibly related PRs

Suggested reviewers

  • sacOO7
  • lawrence-forooghian
  • umair-ably
  • AndyTWF

🐇 In the code, we hop and play,
Adding tests to brighten the day.
With Ktor in our basket tight,
Integration tests now take flight!
Sandbox ready, let’s code away! 🌟


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Outside diff range and nitpick comments (4)
chat-android/src/test/java/com/ably/chat/SandboxTest.kt (2)

29-41: Improve helper functions with constants and validation.

Consider the following improvements:

  1. Move hardcoded values to constants
  2. Add API key validation
  3. Generate unique client IDs to prevent conflicts in parallel tests

Consider this implementation:

+private companion object {
+    const val SANDBOX_ENV = "sandbox"
+    const val API_KEY_PATTERN = "^[^:]+:[^:]+\$"
+}

 private fun createSandboxChatClient(apiKey: String): DefaultChatClient {
+    require(apiKey.matches(Regex(API_KEY_PATTERN))) { "Invalid API key format" }
     val realtime = createSandboxRealtime(apiKey)
     return DefaultChatClient(realtime, ClientOptions())
 }

-private fun createSandboxRealtime(chatApiKey: String, chatClientId: String = "sandbox-client"): AblyRealtime =
+private fun createSandboxRealtime(
+    chatApiKey: String,
+    chatClientId: String = "sandbox-client-${System.currentTimeMillis()}"
+): AblyRealtime =
     AblyRealtime(
         PubSubClientOptions().apply {
             key = chatApiKey
-            environment = "sandbox"
+            environment = SANDBOX_ENV
             clientId = chatClientId
         },
     )

11-11: Add KDoc documentation for the test class.

Since this is a sandbox test setup that others might use as a reference, consider adding documentation explaining:

  • The purpose of these tests
  • Prerequisites for running them
  • Expected behavior
  • Any limitations or considerations

Add this documentation:

+/**
+ * Integration tests using Ably's sandbox environment.
+ * 
+ * Prerequisites:
+ * - Network access to Ably's sandbox environment
+ * - Valid API key creation permissions
+ *
+ * Note: These tests interact with actual sandbox services and may take longer to execute
+ * than unit tests. They should not be mixed with fast-running unit tests.
+ */
 class SandboxTest {
gradle/libs.versions.toml (2)

54-55: Consider adding additional Ktor modules for testing.

For sandbox testing, you might need additional Ktor modules:

  • ktor-client-content-negotiation for JSON handling
  • ktor-serialization-gson for integration with your existing Gson dependency
 ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
 ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
+ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
+ktor-serialization-gson = { module = "io.ktor:ktor-client-gson", version.ref = "ktor" }

57-58: Update bundle if additional Ktor modules are added.

If you add the suggested Ktor modules, update the bundle accordingly:

-ktor-client = ["ktor-client-core", "ktor-client-cio"]
+ktor-client = ["ktor-client-core", "ktor-client-cio", "ktor-client-content-negotiation", "ktor-serialization-gson"]
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 4861812 and 792b70e.

📒 Files selected for processing (4)
  • chat-android/build.gradle.kts (1 hunks)
  • chat-android/src/test/java/com/ably/chat/SandboxTest.kt (1 hunks)
  • chat-android/src/test/java/com/ably/chat/SandboxUtils.kt (1 hunks)
  • gradle/libs.versions.toml (2 hunks)
🔇 Additional comments (4)
chat-android/src/test/java/com/ably/chat/SandboxTest.kt (1)

1-10: LGTM: Imports are well-organized and necessary.

The imports cover all required functionality for sandbox testing, including Ably's realtime capabilities, coroutine testing, and JUnit assertions.

chat-android/build.gradle.kts (1)

52-52: LGTM! The Ktor client dependency is appropriately scoped.

The addition of the Ktor client bundle for testing purposes aligns well with the PR objectives of setting up a sandbox for integration testing.

Let's verify the Ktor client bundle definition in the version catalog:

✅ Verification successful

Ktor client bundle is properly defined in the version catalog

The verification confirms that:

  • The Ktor version is defined: ktor = "3.0.1"
  • The required Ktor client libraries are properly declared:
    • ktor-client-core
    • ktor-client-cio
  • The bundle ktor-client correctly includes both dependencies
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the Ktor client bundle definition exists in the version catalog
# Expected: Should find the bundle definition and its version

# Test: Search for Ktor-related configurations
rg -A 5 'ktor' gradle/libs.versions.toml

Length of output: 770

chat-android/src/test/java/com/ably/chat/SandboxUtils.kt (1)

1-32: Address security concerns in sandbox setup.

Several security considerations need to be addressed:

  1. Implement rate limiting to prevent API abuse
  2. Ensure API keys are not logged
  3. Consider implementing secure storage for sensitive data
  4. Add request timeouts to prevent hanging tests

Let's verify the current usage of this utility:

gradle/libs.versions.toml (1)

23-23: Consider grouping related version declarations.

The Ktor version declaration could be grouped with other networking-related dependencies for better organization.

Let's verify Ktor's compatibility with the project's Kotlin version:

Comment on lines 20 to 25
@Test
fun basicIntegrationTest() = runTest {
val chatClient = createSandboxChatClient(sandboxApiKey)
val room = chatClient.rooms.get("basketball")
room.attach()
assertEquals(ChannelState.attached, room.messages.channel.state)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance test coverage with additional scenarios and proper cleanup.

The current test only covers the basic "happy path". Consider:

  1. Adding timeout handling for attach operation
  2. Testing error scenarios
  3. Adding proper resource cleanup

Consider this enhanced implementation:

 @Test
 fun basicIntegrationTest() = runTest {
     val chatClient = createSandboxChatClient(sandboxApiKey)
-    val room = chatClient.rooms.get("basketball")
-    room.attach()
-    assertEquals(ChannelState.attached, room.messages.channel.state)
+    try {
+        val room = chatClient.rooms.get("basketball")
+        withTimeout(5000) {
+            room.attach()
+        }
+        assertEquals(ChannelState.attached, room.messages.channel.state)
+    } finally {
+        chatClient.close()
+    }
 }

+@Test
+fun shouldHandleAttachFailure() = runTest {
+    val chatClient = createSandboxChatClient("invalid.key")
+    try {
+        val room = chatClient.rooms.get("basketball")
+        withTimeout(5000) {
+            room.attach()
+        }
+        fail("Expected attachment to fail")
+    } catch (e: Exception) {
+        assertTrue(e.message?.contains("invalid key") ?: false)
+    } finally {
+        chatClient.close()
+    }
}

Committable suggestion skipped: line range outside the PR's diff.

chat-android/src/test/java/com/ably/chat/SandboxTest.kt Outdated Show resolved Hide resolved
import io.ktor.http.ContentType
import io.ktor.http.contentType

val client = HttpClient(CIO)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider improving HttpClient configuration and lifecycle management.

The global HttpClient instance could cause issues in parallel tests and lacks important configurations:

  1. Consider making it private and/or scoped to test instances
  2. Add timeout configurations
  3. Implement proper resource cleanup
-val client = HttpClient(CIO)
+private val client = HttpClient(CIO) {
+    engine {
+        requestTimeout = 5000
+    }
+    install(HttpTimeout) {
+        requestTimeoutMillis = 5000
+        connectTimeoutMillis = 5000
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val client = HttpClient(CIO)
private val client = HttpClient(CIO) {
engine {
requestTimeout = 5000
}
install(HttpTimeout) {
requestTimeoutMillis = 5000
connectTimeoutMillis = 5000
}
}

chat-android/src/test/java/com/ably/chat/SandboxUtils.kt Outdated Show resolved Hide resolved
chat-android/src/test/java/com/ably/chat/SandboxUtils.kt Outdated Show resolved Hide resolved
gradle/libs.versions.toml Show resolved Hide resolved

val client = HttpClient(CIO)

suspend fun loadAppCreationRequestBody(): JsonElement =
Copy link
Contributor

@sacOO7 sacOO7 Nov 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we create a sandbox wrapper class instead to hide internal complexity, we expose only two util methods that deal with sandbox app.

  suspend fun setup() : Sandbox {
    // creates a test sandbox app
   // key is set as a property of sandbox
  }
  suspend fun tearDown(app: Sandbox) {
   // deletes the given sandbox app
  }
}

We need to make sure we safely close sandbox app resource after usage in the test case

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to delete an app? I tried to find reference to this in the docs or in other SDKs and can't find

Copy link
Collaborator

@AndyTWF AndyTWF Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We occasionally prune the temp apps on sandbox anyway (they're all _tmp*) - its probably beneficial to keep the same app available for a while anyway, incase you want to debug against it / check logs over time.

@ttypic ttypic force-pushed the ECO-5014/simple-sandbox-setup branch 2 times, most recently from f37a148 to 0b019ad Compare November 13, 2024 11:39
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (3)
chat-android/src/test/java/com/ably/chat/SandboxTest.kt (1)

35-47: Enhance helper functions with validation and configuration options

The helper functions could be improved in several ways:

  1. Add API key format validation
  2. Make environment configurable rather than hard-coding "sandbox"
  3. Allow configuration of other important client options (e.g., logLevel for debugging)

Consider this enhanced implementation:

-private fun createSandboxChatClient(apiKey: String): DefaultChatClient {
+private fun createSandboxChatClient(
+    apiKey: String,
+    options: ClientOptions = ClientOptions(),
+): DefaultChatClient {
+    require(apiKey.contains(":")) { "Invalid API key format" }
     val realtime = createSandboxRealtime(apiKey)
-    return DefaultChatClient(realtime, ClientOptions())
+    return DefaultChatClient(realtime, options)
 }

-private fun createSandboxRealtime(chatApiKey: String, chatClientId: String = "sandbox-client"): AblyRealtime =
+private fun createSandboxRealtime(
+    chatApiKey: String,
+    chatClientId: String = "sandbox-client",
+    environment: String = "sandbox",
+    logLevel: Int? = null,
+): AblyRealtime =
     AblyRealtime(
         PubSubClientOptions().apply {
             key = chatApiKey
-            environment = "sandbox"
+            environment = environment
             clientId = chatClientId
+            logLevel?.let { this.logLevel = it }
         },
     )
chat-android/src/test/java/com/ably/chat/Sandbox.kt (2)

18-23: Consider making URLs configurable

The sandbox URLs should be configurable to support different environments or testing scenarios.

Consider adding configuration parameters:

 class Sandbox {
+    companion object {
+        private const val DEFAULT_SANDBOX_URL = "https://sandbox-rest.ably.io"
+    }
+    
+    private val sandboxUrl: String
+    
+    constructor(sandboxUrl: String = DEFAULT_SANDBOX_URL) {
+        this.sandboxUrl = sandboxUrl
+    }
     
     private lateinit var appId: String
     lateinit var apiKey: String
         private set

1-45: Consider improving test isolation

The current sandbox implementation might affect other tests running in parallel. Consider implementing proper test isolation:

  1. Use separate sandbox instances for different test suites
  2. Implement proper cleanup in case of test failures
  3. Consider using test containers or similar technology for true isolation

Would you like assistance in implementing a more isolated testing approach?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 792b70e and 0b019ad.

📒 Files selected for processing (4)
  • chat-android/build.gradle.kts (1 hunks)
  • chat-android/src/test/java/com/ably/chat/Sandbox.kt (1 hunks)
  • chat-android/src/test/java/com/ably/chat/SandboxTest.kt (1 hunks)
  • gradle/libs.versions.toml (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • chat-android/build.gradle.kts
  • gradle/libs.versions.toml
🔇 Additional comments (1)
chat-android/src/test/java/com/ably/chat/SandboxTest.kt (1)

26-32: 🛠️ Refactor suggestion

Enhance test robustness and resource management

While I agree with the previous review comment about adding timeout handling and error scenarios, there are additional concerns:

  1. The chat client should be properly closed to avoid resource leaks
  2. The test assumes the "basketball" room exists and is accessible
  3. The test doesn't verify if messages can actually be sent/received

Let's verify if there are any similar tests in the codebase that might provide patterns for proper resource management:

#!/bin/bash
# Search for similar test patterns in the codebase
rg -l "AblyRealtime.*close\(\)" --type kotlin
rg "fun.*Test.*runTest" --type kotlin -A 5

chat-android/src/test/java/com/ably/chat/SandboxTest.kt Outdated Show resolved Hide resolved
Comment on lines 35 to 37
suspend fun tearDown() {
client.delete("https://sandbox-rest.ably.io/apps/$appId")
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling to tearDown method

The tearDown method should handle potential failures gracefully.

Consider this implementation:

 suspend fun tearDown() {
-    client.delete("https://sandbox-rest.ably.io/apps/$appId")
+    try {
+        val response = client.delete("$sandboxUrl/apps/$appId")
+        if (!response.status.isSuccess()) {
+            throw SandboxTeardownException("Failed to delete sandbox app: ${response.status}")
+        }
+    } catch (e: Exception) {
+        throw SandboxTeardownException("Failed to delete sandbox app", e)
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
suspend fun tearDown() {
client.delete("https://sandbox-rest.ably.io/apps/$appId")
}
suspend fun tearDown() {
try {
val response = client.delete("$sandboxUrl/apps/$appId")
if (!response.status.isSuccess()) {
throw SandboxTeardownException("Failed to delete sandbox app: ${response.status}")
}
} catch (e: Exception) {
throw SandboxTeardownException("Failed to delete sandbox app", e)
}
}

chat-android/src/test/java/com/ably/chat/Sandbox.kt Outdated Show resolved Hide resolved
Comment on lines 24 to 39
suspend fun setUp() {
val response: HttpResponse = client.post("https://sandbox-rest.ably.io/apps") {
contentType(ContentType.Application.Json)
setBody(loadAppCreationRequestBody().toString())
}
val body = JsonParser.parseString(response.bodyAsText())
// From JS chat repo at 7985ab7 — "The key we need to use is the one at index 5, which gives enough permissions to interact with Chat and Channels"
apiKey = body.asJsonObject["keys"].asJsonArray[5].asJsonObject["keyStr"].asString
appId = body.asJsonObject["appId"].asString
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling and response validation

The setUp method lacks proper error handling and response validation:

  1. No handling of HTTP errors
  2. No validation of response structure
  3. Magic number (index 5) used for key selection

Consider this implementation:

 suspend fun setUp() {
-    val response: HttpResponse = client.post("https://sandbox-rest.ably.io/apps") {
+    val response: HttpResponse = try {
+        client.post("$sandboxUrl/apps") {
+            contentType(ContentType.Application.Json)
+            setBody(loadAppCreationRequestBody().toString())
+        }
+    } catch (e: Exception) {
+        throw SandboxSetupException("Failed to create sandbox app", e)
+    }
+
+    if (!response.status.isSuccess()) {
+        throw SandboxSetupException("Failed to create sandbox app: ${response.status}")
+    }
+
     val body = JsonParser.parseString(response.bodyAsText())
-    apiKey = body.asJsonObject["keys"].asJsonArray[5].asJsonObject["keyStr"].asString
-    appId = body.asJsonObject["appId"].asString
+    try {
+        val keys = body.asJsonObject["keys"]?.asJsonArray
+            ?: throw SandboxSetupException("Missing 'keys' in response")
+        
+        if (keys.size() <= REQUIRED_KEY_INDEX) {
+            throw SandboxSetupException("Insufficient keys in response")
+        }
+        
+        apiKey = keys[REQUIRED_KEY_INDEX].asJsonObject["keyStr"]?.asString
+            ?: throw SandboxSetupException("Invalid key structure")
+        
+        appId = body.asJsonObject["appId"]?.asString
+            ?: throw SandboxSetupException("Missing 'appId' in response")
+    } catch (e: Exception) {
+        throw SandboxSetupException("Failed to parse sandbox response", e)
+    }
 }

+private companion object {
+    // The key at index 5 gives enough permissions to interact with Chat and Channels
+    private const val REQUIRED_KEY_INDEX = 5
+}

+class SandboxSetupException : RuntimeException {
+    constructor(message: String) : super(message)
+    constructor(message: String, cause: Throwable) : super(message, cause)
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
suspend fun setUp() {
val response: HttpResponse = client.post("https://sandbox-rest.ably.io/apps") {
contentType(ContentType.Application.Json)
setBody(loadAppCreationRequestBody().toString())
}
val body = JsonParser.parseString(response.bodyAsText())
// From JS chat repo at 7985ab7 — "The key we need to use is the one at index 5, which gives enough permissions to interact with Chat and Channels"
apiKey = body.asJsonObject["keys"].asJsonArray[5].asJsonObject["keyStr"].asString
appId = body.asJsonObject["appId"].asString
}
suspend fun setUp() {
val response: HttpResponse = try {
client.post("$sandboxUrl/apps") {
contentType(ContentType.Application.Json)
setBody(loadAppCreationRequestBody().toString())
}
} catch (e: Exception) {
throw SandboxSetupException("Failed to create sandbox app", e)
}
if (!response.status.isSuccess()) {
throw SandboxSetupException("Failed to create sandbox app: ${response.status}")
}
val body = JsonParser.parseString(response.bodyAsText())
try {
val keys = body.asJsonObject["keys"]?.asJsonArray
?: throw SandboxSetupException("Missing 'keys' in response")
if (keys.size() <= REQUIRED_KEY_INDEX) {
throw SandboxSetupException("Insufficient keys in response")
}
apiKey = keys[REQUIRED_KEY_INDEX].asJsonObject["keyStr"]?.asString
?: throw SandboxSetupException("Invalid key structure")
appId = body.asJsonObject["appId"]?.asString
?: throw SandboxSetupException("Missing 'appId' in response")
} catch (e: Exception) {
throw SandboxSetupException("Failed to parse sandbox response", e)
}
}
private companion object {
// The key at index 5 gives enough permissions to interact with Chat and Channels
private const val REQUIRED_KEY_INDEX = 5
}
class SandboxSetupException : RuntimeException {
constructor(message: String) : super(message)
constructor(message: String, cause: Throwable) : super(message, cause)
}

Comment on lines 40 to 61
suspend fun loadAppCreationRequestBody(): JsonElement =
JsonParser.parseString(
client.get("https://raw.githubusercontent.com/ably/ably-common/refs/heads/main/test-resources/test-app-setup.json") {
contentType(ContentType.Application.Json)
}.bodyAsText(),
).asJsonObject.get("post_apps")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve configuration loading reliability

The current implementation has several potential issues:

  1. Tests depend on external GitHub availability
  2. Configuration is fetched for every test
  3. No error handling for network issues

Consider caching the configuration and adding fallback:

+private var cachedConfig: JsonElement? = null
+
 suspend fun loadAppCreationRequestBody(): JsonElement =
-    JsonParser.parseString(
-        client.get("https://raw.githubusercontent.com/ably/ably-common/refs/heads/main/test-resources/test-app-setup.json") {
-            contentType(ContentType.Application.Json)
-        }.bodyAsText(),
-    ).asJsonObject.get("post_apps")
+    cachedConfig ?: try {
+        val response = client.get(CONFIG_URL) {
+            contentType(ContentType.Application.Json)
+        }
+        if (!response.status.isSuccess()) {
+            throw ConfigLoadException("Failed to load config: ${response.status}")
+        }
+        JsonParser.parseString(response.bodyAsText())
+            .asJsonObject
+            .get("post_apps")
+            ?.also { cachedConfig = it }
+            ?: throw ConfigLoadException("Missing 'post_apps' in config")
+    } catch (e: Exception) {
+        // Fallback to embedded config if available
+        loadEmbeddedConfig()
+            ?.also { cachedConfig = it }
+            ?: throw ConfigLoadException("Failed to load config", e)
+    }
+
+private companion object {
+    private const val CONFIG_URL =
+        "https://raw.githubusercontent.com/ably/ably-common/refs/heads/main/test-resources/test-app-setup.json"
+}

Consider also embedding a copy of the configuration in the test resources for reliability.

Committable suggestion skipped: line range outside the PR's diff.

@ttypic ttypic requested a review from sacOO7 November 13, 2024 15:15
@Test
fun basicIntegrationTest() = runTest {
val chatClient = createSandboxChatClient(sandbox.apiKey)
val room = chatClient.rooms.get("basketball")
Copy link
Collaborator

@AndyTWF AndyTWF Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd definitely recommend the room id here (and anything you do in tests) being random - if you're running an IDE that caches the sandbox config (or just running multiple tests in a suite), it gets flakey :D


val client = HttpClient(CIO)

class Sandbox {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps not urgent, but would definitely recommend a setting/mode that can work with a local development realtime environment, will make future debugging a lot easier

added basic sandbox setup for simple happy-path testing
@ttypic ttypic force-pushed the ECO-5014/simple-sandbox-setup branch from 0b019ad to 2b17388 Compare November 13, 2024 19:35
@ttypic ttypic requested a review from sacOO7 November 13, 2024 19:37
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (2)
chat-android/src/test/java/com/ably/chat/SandboxTest.kt (1)

10-26: Consider implementing a test fixture pattern

To improve test maintainability and reduce duplication in future test cases, consider:

  1. Creating a base test class with common setup/teardown logic
  2. Implementing test fixtures for common scenarios
  3. Adding helper methods for verification and assertions

Example structure:

abstract class BaseSandboxTest {
    protected lateinit var sandbox: Sandbox
    protected lateinit var chatClient: ChatClient
    
    @Before
    fun setUp() = runTest {
        // Common setup logic
    }
    
    @After
    fun tearDown() = runTest {
        // Common cleanup logic
    }
    
    protected suspend fun createAndAttachRoom(roomId: String = UUID.randomUUID().toString()): Room {
        // Common room creation and attachment logic
    }
}

class SandboxTest : BaseSandboxTest() {
    // Current test methods
}
chat-android/src/test/java/com/ably/chat/Sandbox.kt (1)

18-21: Consider making retry configuration more flexible

While the retry logic is good, consider making it configurable through constructor parameters or environment variables to allow different retry strategies in different test scenarios.

-    install(HttpRequestRetry) {
-        retryOnServerErrors(maxRetries = 4)
-        exponentialDelay()
-    }
+    install(HttpRequestRetry) {
+        retryOnServerErrors(maxRetries = getEnvOrDefault("SANDBOX_MAX_RETRIES", 4))
+        exponentialDelay(
+            base = getEnvOrDefault("SANDBOX_RETRY_BASE_DELAY", 500L),
+            maxDelayMs = getEnvOrDefault("SANDBOX_RETRY_MAX_DELAY", 5000L)
+        )
+    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 0b019ad and 2b17388.

📒 Files selected for processing (4)
  • chat-android/build.gradle.kts (1 hunks)
  • chat-android/src/test/java/com/ably/chat/Sandbox.kt (1 hunks)
  • chat-android/src/test/java/com/ably/chat/SandboxTest.kt (1 hunks)
  • gradle/libs.versions.toml (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • chat-android/build.gradle.kts
  • gradle/libs.versions.toml
🔇 Additional comments (2)
chat-android/src/test/java/com/ably/chat/SandboxTest.kt (1)

19-25: 🛠️ Refactor suggestion

Enhance test robustness and coverage

The current test implementation needs improvements in several areas:

  1. Timeout handling for the attach operation
  2. Resource cleanup for the chat client
  3. Coverage of error scenarios
 @Test
 fun basicIntegrationTest() = runTest {
-    val chatClient = sandbox.createSandboxChatClient()
-    val room = chatClient.rooms.get(UUID.randomUUID().toString())
-    room.attach()
-    assertEquals(ChannelState.attached, room.messages.channel.state)
+    val chatClient = sandbox.createSandboxChatClient()
+    try {
+        val room = chatClient.rooms.get(UUID.randomUUID().toString())
+        withTimeout(5000) {
+            room.attach()
+        }
+        assertEquals(ChannelState.attached, room.messages.channel.state)
+    } finally {
+        chatClient.close()
+    }
 }
+
+@Test
+fun shouldHandleAttachFailure() = runTest {
+    val chatClient = sandbox.createSandboxChatClient()
+    try {
+        val room = chatClient.rooms.get("invalid-room-id")
+        assertThrows<AblyException> {
+            withTimeout(5000) {
+                room.attach()
+            }
+        }
+    } finally {
+        chatClient.close()
+    }
}
chat-android/src/test/java/com/ably/chat/Sandbox.kt (1)

42-45: ⚠️ Potential issue

Fix incorrect parameter usage in createSandboxChatClient

The function passes apiKey instead of chatClientId to createSandboxRealtime.

-    val realtime = createSandboxRealtime(apiKey)
+    val realtime = createSandboxRealtime()

Likely invalid or redundant comment.

Comment on lines +14 to +17
@Before
fun setUp() = runTest {
sandbox = Sandbox.createInstance()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling and cleanup to test lifecycle methods

The current setup lacks error handling and proper cleanup. Consider adding:

  1. Error handling in setUp
  2. A tearDown method to clean up resources
  3. Timeout handling for sandbox creation
 @Before
 fun setUp() = runTest {
-    sandbox = Sandbox.createInstance()
+    try {
+        withTimeout(5000) {
+            sandbox = Sandbox.createInstance()
+        }
+    } catch (e: Exception) {
+        throw IllegalStateException("Failed to set up sandbox environment: ${e.message}", e)
+    }
 }
+
+@After
+fun tearDown() = runTest {
+    try {
+        sandbox.close()
+    } catch (e: Exception) {
+        println("Warning: Failed to tear down sandbox environment: ${e.message}")
+        throw e
+    }
+}

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +27 to +30
val response: HttpResponse = client.post("https://sandbox-rest.ably.io/apps") {
contentType(ContentType.Application.Json)
setBody(loadAppCreationRequestBody().toString())
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make sandbox URL configurable

The sandbox URL should be configurable to support different environments (e.g., local development as suggested in past reviews).

+    companion object {
+        private const val DEFAULT_SANDBOX_URL = "https://sandbox-rest.ably.io"
+        private val SANDBOX_URL = System.getenv("SANDBOX_URL") ?: DEFAULT_SANDBOX_URL
+    }

-            val response: HttpResponse = client.post("https://sandbox-rest.ably.io/apps") {
+            val response: HttpResponse = client.post("$SANDBOX_URL/apps") {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val response: HttpResponse = client.post("https://sandbox-rest.ably.io/apps") {
contentType(ContentType.Application.Json)
setBody(loadAppCreationRequestBody().toString())
}
companion object {
private const val DEFAULT_SANDBOX_URL = "https://sandbox-rest.ably.io"
private val SANDBOX_URL = System.getenv("SANDBOX_URL") ?: DEFAULT_SANDBOX_URL
}
val response: HttpResponse = client.post("$SANDBOX_URL/apps") {
contentType(ContentType.Application.Json)
setBody(loadAppCreationRequestBody().toString())
}

Comment on lines +56 to +61
private suspend fun loadAppCreationRequestBody(): JsonElement =
JsonParser.parseString(
client.get("https://raw.githubusercontent.com/ably/ably-common/refs/heads/main/test-resources/test-app-setup.json") {
contentType(ContentType.Application.Json)
}.bodyAsText(),
).asJsonObject.get("post_apps")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add timeout for configuration loading

Configuration loading from external URL should have a reasonable timeout to prevent tests from hanging.

+private const val CONFIG_TIMEOUT_MS = 5000L

 private suspend fun loadAppCreationRequestBody(): JsonElement =
     JsonParser.parseString(
         client.get("https://raw.githubusercontent.com/ably/ably-common/refs/heads/main/test-resources/test-app-setup.json") {
+            timeout {
+                requestTimeoutMillis = CONFIG_TIMEOUT_MS
+            }
             contentType(ContentType.Application.Json)
         }.bodyAsText(),
     ).asJsonObject.get("post_apps")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private suspend fun loadAppCreationRequestBody(): JsonElement =
JsonParser.parseString(
client.get("https://raw.githubusercontent.com/ably/ably-common/refs/heads/main/test-resources/test-app-setup.json") {
contentType(ContentType.Application.Json)
}.bodyAsText(),
).asJsonObject.get("post_apps")
private const val CONFIG_TIMEOUT_MS = 5000L
private suspend fun loadAppCreationRequestBody(): JsonElement =
JsonParser.parseString(
client.get("https://raw.githubusercontent.com/ably/ably-common/refs/heads/main/test-resources/test-app-setup.json") {
timeout {
requestTimeoutMillis = CONFIG_TIMEOUT_MS
}
contentType(ContentType.Application.Json)
}.bodyAsText(),
).asJsonObject.get("post_apps")

Comment on lines +47 to +54
internal fun Sandbox.createSandboxRealtime(chatClientId: String = "sandbox-client"): AblyRealtime =
AblyRealtime(
io.ably.lib.types.ClientOptions().apply {
key = apiKey
environment = "sandbox"
clientId = chatClientId
},
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Generate unique client IDs for parallel test safety

Using a fixed default client ID could cause issues in parallel test execution.

-internal fun Sandbox.createSandboxRealtime(chatClientId: String = "sandbox-client"): AblyRealtime =
+internal fun Sandbox.createSandboxRealtime(
+    chatClientId: String = "sandbox-client-${System.currentTimeMillis()}-${Random.nextInt(1000)}"
+): AblyRealtime =
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
internal fun Sandbox.createSandboxRealtime(chatClientId: String = "sandbox-client"): AblyRealtime =
AblyRealtime(
io.ably.lib.types.ClientOptions().apply {
key = apiKey
environment = "sandbox"
clientId = chatClientId
},
)
internal fun Sandbox.createSandboxRealtime(
chatClientId: String = "sandbox-client-${System.currentTimeMillis()}-${Random.nextInt(1000)}"
): AblyRealtime =
AblyRealtime(
io.ably.lib.types.ClientOptions().apply {
key = apiKey
environment = "sandbox"
clientId = chatClientId
},
)

Copy link
Contributor

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ttypic ttypic merged commit d5f5e82 into main Nov 14, 2024
2 checks passed
@ttypic ttypic deleted the ECO-5014/simple-sandbox-setup branch November 14, 2024 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants