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