Skip to content

Commit de1ecd0

Browse files
committed
feat: added tests for port forwarding
Signed-off-by: Andre Dietisheim <adietish@redhat.com>
1 parent 2d2316f commit de1ecd0

File tree

3 files changed

+153
-8
lines changed

3 files changed

+153
-8
lines changed

build.gradle.kts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ repositories {
3131

3232
// Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog
3333
dependencies {
34-
testImplementation(libs.junit)
34+
testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.0")
35+
testImplementation("org.junit.platform:junit-platform-launcher:1.11.0")
36+
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.0")
37+
testImplementation("org.assertj:assertj-core:3.23.1")
38+
testImplementation("io.mockk:mockk:1.14.6")
3539

3640
// IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html
3741
intellijPlatform {
@@ -136,6 +140,11 @@ tasks {
136140
publishPlugin {
137141
dependsOn(patchChangelog)
138142
}
143+
144+
withType<Test> {
145+
useJUnitPlatform()
146+
jvmArgs("-Dnet.bytebuddy.experimental=true", "-Dmockk.agent.global=false")
147+
}
139148
}
140149

141150
intellijPlatformTesting {

src/main/kotlin/com/redhat/devtools/gateway/openshift/Pods.kt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
package com.redhat.devtools.gateway.openshift
1313

1414
import com.intellij.openapi.diagnostic.logger
15-
import com.redhat.devtools.gateway.view.ui.Dialogs
1615
import io.kubernetes.client.Exec
1716
import io.kubernetes.client.PortForward
1817
import io.kubernetes.client.openapi.ApiClient
@@ -113,9 +112,7 @@ class Pods(private val client: ApiClient) {
113112
// dont cancel if child coroutine fails + use blocking I/O scope
114113
SupervisorJob() + Dispatchers.IO
115114
)
116-
117115
scope.acceptConnections(serverSocket, pod, localPort, remotePort)
118-
119116
return Closeable {
120117
runCatching { serverSocket.close() }
121118
scope.cancel()
@@ -191,12 +188,9 @@ class Pods(private val client: ApiClient) {
191188
}
192189
}
193190
} catch(e: Exception) {
194-
logger.info(
191+
logger.warn(
195192
"Could not port forward to pod ${pod.metadata?.name} using port $localPort -> $remotePort",
196193
e)
197-
Dialogs.error(
198-
"Could not port forward to pod ${pod.metadata?.name} using port $localPort -> $remotePort: ${e.message}",
199-
"Port Forward Error")
200194
} finally {
201195
runCatching { clientSocket.close() }
202196
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright (c) 2025 Red Hat, Inc.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Red Hat, Inc. - initial API and implementation
11+
*/
12+
package com.redhat.devtools.gateway.openshift
13+
14+
import io.kubernetes.client.PortForward
15+
import io.kubernetes.client.openapi.ApiClient
16+
import io.kubernetes.client.openapi.models.V1ObjectMeta
17+
import io.kubernetes.client.openapi.models.V1Pod
18+
import io.mockk.every
19+
import io.mockk.mockk
20+
import io.mockk.mockkConstructor
21+
import io.mockk.unmockkConstructor
22+
import io.mockk.verify
23+
import kotlinx.coroutines.delay
24+
import kotlinx.coroutines.runBlocking
25+
import org.assertj.core.api.Assertions.assertThat
26+
import org.junit.jupiter.api.AfterEach
27+
import org.junit.jupiter.api.BeforeEach
28+
import org.junit.jupiter.api.Test
29+
import java.io.ByteArrayInputStream
30+
import java.io.ByteArrayOutputStream
31+
import java.io.IOException
32+
import java.net.ServerSocket
33+
import java.net.Socket
34+
35+
class PodsTest {
36+
37+
private val serverData = "from server"
38+
39+
private lateinit var client: ApiClient
40+
private lateinit var pods: Pods
41+
42+
private lateinit var buffer: ByteArray
43+
44+
private val pod = V1Pod().apply {
45+
metadata = V1ObjectMeta().apply {
46+
name = "luke-skywalker"
47+
}
48+
}
49+
50+
private val remotePort = 8080
51+
private var localPort = 0
52+
53+
@BeforeEach
54+
fun setUp() {
55+
client = mockk(relaxed = true)
56+
pods = Pods(client)
57+
localPort = findFreePort()
58+
buffer = ByteArray(1024)
59+
mockkConstructor(PortForward::class)
60+
}
61+
62+
@AfterEach
63+
fun tearDown() {
64+
unmockkConstructor(PortForward::class)
65+
}
66+
67+
@Test
68+
fun `#forward copies from server to client`() {
69+
// given
70+
val portForwardResult = mockk<PortForward.PortForwardResult>(relaxed = true)
71+
every {
72+
anyConstructed<PortForward>().forward(pod, listOf(remotePort))
73+
} returns portForwardResult
74+
val serverIn = ByteArrayInputStream(serverData.toByteArray())
75+
every {
76+
portForwardResult.getInputStream(remotePort)
77+
} returns serverIn
78+
val serverOut = ByteArrayOutputStream()
79+
every {
80+
portForwardResult.getOutboundStream(remotePort)
81+
} returns serverOut
82+
83+
// when
84+
val closeable = pods.forward(pod, localPort, remotePort)
85+
86+
// then
87+
// wait for the server to start
88+
runBlocking { delay(100) }
89+
90+
try {
91+
// Verify that data from server input stream is received by client
92+
val bytesRead = sendClientData("ping") // Send data to trigger server response
93+
assertThat(String(buffer, 0, bytesRead)).isEqualTo(serverData)
94+
} finally {
95+
closeable.close()
96+
}
97+
}
98+
99+
@Test
100+
fun `#forward tries several times if connecting fails`() {
101+
// given
102+
every {
103+
anyConstructed<PortForward>().forward(pod, listOf(remotePort))
104+
} throws mockk<IOException>(relaxed = true)
105+
106+
// when
107+
val closeable = pods.forward(pod, localPort, remotePort)
108+
109+
// then
110+
// wait for the server to start
111+
runBlocking { delay(100) }
112+
Socket("127.0.0.1", localPort).apply {
113+
close() // trigger retry
114+
}
115+
runBlocking { delay(6000) } // 5 attempts * 1 second
116+
117+
try {
118+
verify(atLeast = 2) { // 2+ retries
119+
anyConstructed<PortForward>().forward(pod, listOf(remotePort))
120+
}
121+
} finally {
122+
closeable.close()
123+
}
124+
}
125+
126+
private fun sendClientData(data: String): Int {
127+
Socket("127.0.0.1", localPort).use {
128+
// client to server
129+
runCatching {
130+
it.outputStream.write(data.toByteArray())
131+
it.outputStream.flush()
132+
}
133+
134+
// server to client
135+
return it.inputStream.read(buffer)
136+
}
137+
}
138+
139+
private fun findFreePort(): Int {
140+
return ServerSocket(0).use { it.localPort }
141+
}
142+
}

0 commit comments

Comments
 (0)