Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
chore: simplify uri handling implementation (5)
Removed unused code and refactored unit tests to take into account that some of the methods are no longer
static, and that return type changed.
  • Loading branch information
fioan89 committed May 29, 2025
commit 122db33e70ce6b9e349560b51c71899f838a4b23
44 changes: 1 addition & 43 deletions src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,14 @@ import com.coder.toolbox.sdk.CoderRestClient
import com.coder.toolbox.sdk.v2.models.Workspace
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
import com.jetbrains.toolbox.api.localization.LocalizableString
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.time.withTimeout
import java.net.HttpURLConnection
import java.net.URI
import java.net.URL
import java.util.UUID
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
Expand Down Expand Up @@ -227,7 +224,7 @@ open class CoderProtocolHandler(
*
* @throws [IllegalArgumentException]
*/
private suspend fun getMatchingAgent(
internal suspend fun getMatchingAgent(
parameters: Map<String, String?>,
workspace: Workspace,
): WorkspaceAgent? {
Expand All @@ -238,7 +235,6 @@ open class CoderProtocolHandler(
}

// If the agent is missing and the workspace has only one, use that.
// Prefer the ID over the name if both are set.
val agent =
if (!parameters.agentID().isNullOrBlank()) {
agents.firstOrNull { it.id.toString() == parameters.agentID() }
Expand Down Expand Up @@ -430,44 +426,6 @@ open class CoderProtocolHandler(
}
}

/**
* Follow a URL's redirects to its final destination.
*/
internal fun resolveRedirects(url: URL): URL {
var location = url
val maxRedirects = 10
for (i in 1..maxRedirects) {
val conn = location.openConnection() as HttpURLConnection
conn.instanceFollowRedirects = false
conn.connect()
val code = conn.responseCode
val nextLocation = conn.getHeaderField("Location")
conn.disconnect()
// Redirects are triggered by any code starting with 3 plus a
// location header.
if (code < 300 || code >= 400 || nextLocation.isNullOrBlank()) {
return location
}
// Location headers might be relative.
location = URL(location, nextLocation)
}
throw Exception("Too many redirects")
}

private suspend fun CoderToolboxContext.showErrorPopup(error: Throwable) {
popupPluginMainPage()
this.ui.showErrorInfoPopup(error)
}

private suspend fun CoderToolboxContext.showInfoPopup(
title: LocalizableString,
message: LocalizableString,
okLabel: LocalizableString
) {
popupPluginMainPage()
this.ui.showInfoPopup(title, message, okLabel)
}

private fun CoderToolboxContext.popupPluginMainPage() {
this.ui.showWindow()
this.envPageManager.showPluginEnvironmentsPage(true)
Expand Down
1 change: 0 additions & 1 deletion src/main/kotlin/com/coder/toolbox/util/LinkMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.coder.toolbox.util
const val URL = "url"
const val TOKEN = "token"
const val WORKSPACE = "workspace"
const val AGENT_NAME = "agent"
const val AGENT_ID = "agent_id"
private const val IDE_PRODUCT_CODE = "ide_product_code"
private const val IDE_BUILD_NUMBER = "ide_build_number"
Expand Down
161 changes: 65 additions & 96 deletions src/test/kotlin/com/coder/toolbox/util/LinkHandlerTest.kt
Original file line number Diff line number Diff line change
@@ -1,41 +1,49 @@
package com.coder.toolbox.util

import com.coder.toolbox.CoderToolboxContext
import com.coder.toolbox.sdk.DataGen
import com.sun.net.httpserver.HttpHandler
import com.sun.net.httpserver.HttpServer
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import com.coder.toolbox.settings.Environment
import com.coder.toolbox.store.CoderSecretsStore
import com.coder.toolbox.store.CoderSettingsStore
import com.jetbrains.toolbox.api.core.diagnostics.Logger
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
import com.jetbrains.toolbox.api.ui.ToolboxUi
import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull

internal class LinkHandlerTest {
/**
* Create, start, and return a server that uses the provided handler.
*/
private fun mockServer(handler: HttpHandler): Pair<HttpServer, String> {
val srv = HttpServer.create(InetSocketAddress(0), 0)
srv.createContext("/", handler)
srv.start()
return Pair(srv, "http://localhost:" + srv.address.port)
}

/**
* Create, start, and return a server that mocks redirects.
*/
private fun mockRedirectServer(
location: String,
temp: Boolean,
): Pair<HttpServer, String> = mockServer { exchange ->
exchange.responseHeaders.set("Location", location)
exchange.sendResponseHeaders(
if (temp) HttpURLConnection.HTTP_MOVED_TEMP else HttpURLConnection.HTTP_MOVED_PERM,
-1,
)
exchange.close()
}
private val context = CoderToolboxContext(
mockk<ToolboxUi>(relaxed = true),
mockk<EnvironmentUiPageManager>(),
mockk<EnvironmentStateColorPalette>(),
mockk<RemoteToolsHelper>(),
mockk<ClientHelper>(),
mockk<LocalDesktopManager>(),
mockk<CoroutineScope>(),
mockk<Logger>(relaxed = true),
mockk<LocalizableStringFactory>(relaxed = true),
CoderSettingsStore(pluginTestSettingsStore(), Environment(), mockk<Logger>(relaxed = true)),
mockk<CoderSecretsStore>(),
mockk<ToolboxProxySettings>()
)

private val protocolHandler = CoderProtocolHandler(
context,
DialogUi(context),
MutableStateFlow(false)
)

private val agents =
mapOf(
Expand All @@ -49,7 +57,7 @@ internal class LinkHandlerTest {
)

@Test
fun getMatchingAgent() {
fun tstgetMatchingAgent() {
val ws = DataGen.workspace("ws", agents = agents)

val tests =
Expand All @@ -74,9 +82,10 @@ internal class LinkHandlerTest {
"b0e4c54d-9ba9-4413-8512-11ca1e826a24",
),
)

tests.forEach {
assertEquals(UUID.fromString(it.second), getMatchingAgent(it.first, ws).id)
runBlocking {
tests.forEach {
assertEquals(UUID.fromString(it.second), protocolHandler.getMatchingAgent(it.first, ws)?.id)
}
}
}

Expand Down Expand Up @@ -104,14 +113,10 @@ internal class LinkHandlerTest {
"agent with ID",
),
)

tests.forEach {
val ex =
assertFailsWith(
exceptionClass = it.second,
block = { getMatchingAgent(it.first, ws).id },
)
assertContains(ex.message.toString(), it.third)
runBlocking {
tests.forEach {
assertNull(protocolHandler.getMatchingAgent(it.first, ws)?.id)
}
}
}

Expand All @@ -126,15 +131,16 @@ internal class LinkHandlerTest {
mapOf("agent" to null),
mapOf("agent_id" to null),
)

tests.forEach {
assertEquals(
UUID.fromString("b0e4c54d-9ba9-4413-8512-11ca1e826a24"),
getMatchingAgent(
it,
ws,
).id,
)
runBlocking {
tests.forEach {
assertEquals(
UUID.fromString("b0e4c54d-9ba9-4413-8512-11ca1e826a24"),
protocolHandler.getMatchingAgent(
it,
ws,
)?.id,
)
}
}
}

Expand All @@ -149,14 +155,10 @@ internal class LinkHandlerTest {
"agent with ID"
),
)

tests.forEach {
val ex =
assertFailsWith(
exceptionClass = it.second,
block = { getMatchingAgent(it.first, ws).id },
)
assertContains(ex.message.toString(), it.third)
runBlocking {
tests.forEach {
assertNull(protocolHandler.getMatchingAgent(it.first, ws)?.id)
}
}
}

Expand All @@ -177,43 +179,10 @@ internal class LinkHandlerTest {
"has no agents"
),
)

tests.forEach {
val ex =
assertFailsWith(
exceptionClass = it.second,
block = { getMatchingAgent(it.first, ws).id },
)
assertContains(ex.message.toString(), it.third)
}
}

@Test
fun followsRedirects() {
val (srv1, url1) =
mockServer { exchange ->
exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, -1)
exchange.close()
runBlocking {
tests.forEach {
assertNull(protocolHandler.getMatchingAgent(it.first, ws)?.id)
}
val (srv2, url2) = mockRedirectServer(url1, false)
val (srv3, url3) = mockRedirectServer(url2, true)

assertEquals(url1.toURL(), resolveRedirects(java.net.URL(url3)))

srv1.stop(0)
srv2.stop(0)
srv3.stop(0)
}

@Test
fun followsMaximumRedirects() {
val (srv, url) = mockRedirectServer(".", true)

assertFailsWith(
exceptionClass = Exception::class,
block = { resolveRedirects(java.net.URL(url)) },
)

srv.stop(0)
}
}
}