1
+ /* ******************************************************************************
2
+ * Copyright (c) 2023 Red Hat, Inc.
3
+ * Distributed under license by Red Hat, Inc. All rights reserved.
4
+ * This program is made available under the terms of the
5
+ * Eclipse Public License v2.0 which accompanies this distribution,
6
+ * and is available at http://www.eclipse.org/legal/epl-v20.html
7
+ *
8
+ * Contributors:
9
+ * Red Hat, Inc. - initial API and implementation
10
+ ******************************************************************************/
11
+ package com.redhat.devtools.intellij.kubernetes.model.dashboard
12
+
13
+ import com.nhaarman.mockitokotlin2.any
14
+ import com.nhaarman.mockitokotlin2.doReturn
15
+ import com.nhaarman.mockitokotlin2.doThrow
16
+ import com.nhaarman.mockitokotlin2.mock
17
+ import com.nhaarman.mockitokotlin2.verify
18
+ import com.nhaarman.mockitokotlin2.whenever
19
+ import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.NAMESPACE1
20
+ import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.NAMESPACE2
21
+ import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.NAMESPACE3
22
+ import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.POD1
23
+ import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.POD2
24
+ import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.POD3
25
+ import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.client
26
+ import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.resource
27
+ import com.redhat.devtools.intellij.kubernetes.model.util.PluginException
28
+ import io.fabric8.kubernetes.api.model.Container
29
+ import io.fabric8.kubernetes.api.model.ContainerPort
30
+ import io.fabric8.kubernetes.api.model.ObjectMeta
31
+ import io.fabric8.kubernetes.api.model.Pod
32
+ import io.fabric8.kubernetes.api.model.PodList
33
+ import io.fabric8.kubernetes.api.model.PodSpec
34
+ import io.fabric8.kubernetes.api.model.Service
35
+ import io.fabric8.kubernetes.api.model.ServiceList
36
+ import io.fabric8.kubernetes.api.model.ServiceSpec
37
+ import io.fabric8.kubernetes.client.KubernetesClient
38
+ import io.fabric8.kubernetes.client.KubernetesClientException
39
+ import io.fabric8.kubernetes.client.LocalPortForward
40
+ import io.fabric8.kubernetes.client.dsl.MixedOperation
41
+ import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation
42
+ import io.fabric8.kubernetes.client.dsl.PodResource
43
+ import io.fabric8.kubernetes.client.dsl.ServiceResource
44
+ import java.net.HttpURLConnection
45
+ import org.assertj.core.api.Assertions.assertThat
46
+ import org.junit.Before
47
+ import org.junit.Test
48
+
49
+
50
+ class KubernetesDashboardTest {
51
+
52
+ companion object {
53
+ private const val DASHBOARD_NAMESPACE = " kubernetes-dashboard"
54
+ private const val DASHBOARD_SERVICE_NAME = " kubernetes-dashboard"
55
+ private const val DASHBOARD_POD_NAME = " kubernetes-dashboard"
56
+ private const val DASHBOARD_APP_KEY = " k8s-app"
57
+ private const val DASHBOARD_LABEL = " kubernetes-dashboard"
58
+ }
59
+
60
+ private var client: KubernetesClient = mock()
61
+ private val dashboard = KubernetesDashboard (client, " yoda" , " https://localhost" )
62
+
63
+ @Before
64
+ fun before () {
65
+ this .client = client(NAMESPACE2 .metadata.name, arrayOf(NAMESPACE1 , NAMESPACE2 , NAMESPACE3 ))
66
+ }
67
+
68
+ @Test
69
+ fun `#get should NOT connect a 2nd time if 1st connect was successful` () {
70
+ // given
71
+ val dashboard = FixedResponseDashboard (HttpURLConnection .HTTP_OK )
72
+ dashboard.get()
73
+ // when
74
+ dashboard.get()
75
+ // then
76
+ assertThat(dashboard.connectInvoked).isEqualTo(1 )
77
+ }
78
+
79
+ @Test
80
+ fun `#get should connect a 2nd time if 1st connect was unsuccessful` () {
81
+ // given
82
+ val dashboard = FixedResponseDashboard (HttpURLConnection .HTTP_NOT_FOUND )
83
+ try {
84
+ dashboard.get()
85
+ } catch (e: PluginException ) {
86
+ // will throw because unsuccessful. Ignore it.
87
+ }
88
+ // when
89
+ try {
90
+ dashboard.get()
91
+ } catch (e: PluginException ) {
92
+ // will throw because unsuccessful. Ignore it.
93
+ }
94
+ // then
95
+ assertThat(dashboard.connectInvoked).isEqualTo(2 )
96
+ }
97
+
98
+ @Test(expected = PluginException ::class )
99
+ fun `#get should throw if accessing cluster throws` () {
100
+ // given
101
+ doThrow(KubernetesClientException ::class )
102
+ .whenever(client).services()
103
+ // when
104
+ dashboard.get()
105
+ // then
106
+ }
107
+
108
+ @Test(expected = PluginException ::class )
109
+ fun `#get should throw if service cannot be found` () {
110
+ // given
111
+ // service doesn't exist
112
+ // when
113
+ dashboard.get()
114
+ // then
115
+ }
116
+
117
+ @Test(expected = PluginException ::class )
118
+ fun `#get should throw if pod cannot be found` () {
119
+ // given
120
+ // service exists, pod doesn't exist
121
+ val dashboardService = dashboardService()
122
+ mockDashboardService(dashboardService, client)
123
+
124
+ // when
125
+ dashboard.get()
126
+ // then
127
+ }
128
+
129
+ @Test(expected= PluginException ::class )
130
+ fun `#get should return url if port forward has server errors` () {
131
+ // given
132
+ val expectedPort = 9090
133
+ val expectedUrl = " https://localhost:$expectedPort "
134
+ val dashboardHttpStatusCode = HttpURLConnection .HTTP_FORBIDDEN
135
+
136
+ val dashboardService = dashboardService()
137
+ mockDashboardService(dashboardService, client)
138
+
139
+ val dashboardPod = dashboardPod(8080 )
140
+ val serverError = KubernetesClientException (" dark side of the force was used" )
141
+ val portForward = portForward(expectedPort, listOf (serverError))
142
+ mockDashboardPodAndPortForward(dashboardPod, portForward, client)
143
+
144
+ val dashboard = createKubernetesDashboard(expectedUrl, dashboardHttpStatusCode)
145
+
146
+ // when
147
+ dashboard.get()
148
+ // then
149
+ }
150
+
151
+ @Test
152
+ fun `#get should return url if dashboard responds with 200 OK` () {
153
+ // given
154
+ val expectedPort = 9090
155
+ val expectedUrl = " https://localhost:$expectedPort "
156
+ val dashboardHttpStatusCode = HttpURLConnection .HTTP_OK
157
+
158
+ val dashboardService = dashboardService()
159
+ mockDashboardService(dashboardService, client)
160
+
161
+ val dashboardPod = dashboardPod(8080 )
162
+ val portForward = portForward(expectedPort)
163
+ mockDashboardPodAndPortForward(dashboardPod, portForward, client)
164
+
165
+ val dashboard = createKubernetesDashboard(expectedUrl, dashboardHttpStatusCode)
166
+ // when
167
+ val effectiveUrl = dashboard.get()
168
+ // then
169
+ assertThat(effectiveUrl).isEqualTo(expectedUrl)
170
+ }
171
+
172
+ @Test
173
+ fun `#get should return url if dashboard responds with 403 FORBIDDEN` () {
174
+ // given
175
+ val expectedPort = 9090
176
+ val expectedUrl = " https://localhost:$expectedPort "
177
+ val dashboardHttpStatusCode = HttpURLConnection .HTTP_FORBIDDEN
178
+
179
+ val dashboardService = dashboardService()
180
+ mockDashboardService(dashboardService, client)
181
+
182
+ val dashboardPod = dashboardPod(8080 )
183
+ val portForward = portForward(expectedPort)
184
+ mockDashboardPodAndPortForward(dashboardPod, portForward, client)
185
+
186
+ val dashboard = createKubernetesDashboard(expectedUrl, dashboardHttpStatusCode)
187
+
188
+ // when
189
+ val effectiveUrl = dashboard.get()
190
+ // then
191
+ assertThat(effectiveUrl).isEqualTo(expectedUrl)
192
+ }
193
+
194
+ @Test(expected = PluginException ::class )
195
+ fun `#get should throw if dashboard responds with 500 Internal Server Error` () {
196
+ // given
197
+ val expectedPort = 9090
198
+ val expectedUrl = " https://localhost:$expectedPort "
199
+ val dashboardHttpStatusCode = HttpURLConnection .HTTP_INTERNAL_ERROR
200
+
201
+ val dashboardService = dashboardService()
202
+ mockDashboardService(dashboardService, client)
203
+
204
+ val dashboardPod = dashboardPod(8080 )
205
+ val portForward = portForward(expectedPort)
206
+ mockDashboardPodAndPortForward(dashboardPod, portForward, client)
207
+
208
+ val dashboard = createKubernetesDashboard(expectedUrl, dashboardHttpStatusCode)
209
+ // when
210
+ dashboard.get()
211
+ // then throws
212
+ }
213
+
214
+ @Test
215
+ fun `#close should close existing portforward` () {
216
+ // given
217
+ val expectedPort = 9090
218
+ val expectedUrl = " https://localhost:$expectedPort "
219
+ val dashboardHttpStatusCode = HttpURLConnection .HTTP_OK
220
+
221
+ val dashboardService = dashboardService()
222
+ mockDashboardService(dashboardService, client)
223
+
224
+ val dashboardPod = dashboardPod(8080 )
225
+ val portForward = portForward(expectedPort)
226
+ mockDashboardPodAndPortForward(dashboardPod, portForward, client)
227
+
228
+ val dashboard = createKubernetesDashboard(expectedUrl, dashboardHttpStatusCode)
229
+ dashboard.get() // create port forward
230
+ // when
231
+ dashboard.close()
232
+ // then throws
233
+ verify(portForward).close()
234
+ }
235
+
236
+ private fun mockDashboardService (dashboardService : Service , client : KubernetesClient ) {
237
+ val serviceResource: ServiceResource <Service > = mock {
238
+ on { get() } doReturn dashboardService
239
+ }
240
+ val servicesInNamespace: NonNamespaceOperation <Service , ServiceList , ServiceResource <Service >> = mock {
241
+ on { withName(DASHBOARD_SERVICE_NAME ) } doReturn serviceResource
242
+ }
243
+ val services: MixedOperation <Service , ServiceList , ServiceResource <Service >> = mock {
244
+ on { inNamespace(DASHBOARD_NAMESPACE ) } doReturn servicesInNamespace
245
+ }
246
+ whenever(client.services())
247
+ .thenReturn(services)
248
+ }
249
+
250
+ private fun mockDashboardPodAndPortForward (
251
+ dashboardPod : Pod ,
252
+ portForward : LocalPortForward ,
253
+ client : KubernetesClient
254
+ ) {
255
+ val allPods: PodList = mock {
256
+ on { items } doReturn listOf (POD1 , POD2 , dashboardPod, POD3 )
257
+ }
258
+ val podResource: PodResource = mock {
259
+ on { portForward(any()) } doReturn portForward
260
+ }
261
+ val podsInNamespace: NonNamespaceOperation <Pod , PodList , PodResource > = mock {
262
+ on { list() } doReturn allPods
263
+ on { withName(any()) } doReturn podResource
264
+ }
265
+ val pods: MixedOperation <Pod , PodList , PodResource > = mock {
266
+ on { inNamespace(any()) } doReturn podsInNamespace
267
+ }
268
+ whenever(client.pods())
269
+ .thenReturn(pods)
270
+ }
271
+
272
+ private fun portForward (
273
+ localPort : Int ,
274
+ serverThrowables : List <Throwable > = emptyList(),
275
+ clientThrowables : List <Throwable > = emptyList()
276
+ ): LocalPortForward {
277
+ return mock {
278
+ on { getLocalPort() } doReturn localPort
279
+ on { getServerThrowables() } doReturn serverThrowables
280
+ on { getClientThrowables() } doReturn clientThrowables
281
+ }
282
+ }
283
+
284
+ private fun dashboardService (): Service {
285
+ val spec: ServiceSpec = mock {
286
+ on { selector } doReturn mapOf (DASHBOARD_APP_KEY to DASHBOARD_LABEL )
287
+ }
288
+ return resource<Service >(DASHBOARD_SERVICE_NAME , DASHBOARD_NAMESPACE ).apply {
289
+ doReturn(spec)
290
+ .whenever(this ).spec
291
+ }
292
+ }
293
+
294
+ private fun dashboardPod (port : Int ): Pod {
295
+ val containerPort: ContainerPort = mock {
296
+ on { containerPort } doReturn port
297
+ }
298
+ val container: Container = mock {
299
+ on { ports } doReturn listOf (containerPort)
300
+ }
301
+ val podSpec: PodSpec = mock {
302
+ on { containers } doReturn listOf (container)
303
+ }
304
+ val metadata: ObjectMeta = mock {
305
+ on { name } doReturn DASHBOARD_POD_NAME
306
+ on { namespace } doReturn DASHBOARD_NAMESPACE
307
+ on { labels } doReturn mapOf (DASHBOARD_APP_KEY to DASHBOARD_LABEL )
308
+ }
309
+ return mock {
310
+ on { getMetadata() } doReturn metadata
311
+ on { spec } doReturn podSpec
312
+ }
313
+ }
314
+
315
+ private fun createKubernetesDashboard (expectedUrl : String , httpStatusCode : Int ): KubernetesDashboard {
316
+ val httpRequest: HttpRequest = mockHttpRequest(expectedUrl, httpStatusCode)
317
+ return KubernetesDashboard (client, " yoda" , " Dagobah" , httpRequest)
318
+ }
319
+
320
+ private fun mockHttpRequest (url : String , code : Int ): HttpRequest {
321
+ val httpStatusCode: HttpRequest .HttpStatusCode = HttpRequest .HttpStatusCode (
322
+ url,
323
+ code
324
+ )
325
+ val httpRequest: HttpRequest = mock {
326
+ on { request(any(), any()) } doReturn httpStatusCode
327
+ }
328
+ return httpRequest
329
+ }
330
+
331
+ private class FixedResponseDashboard (private val httpStatusCode : Int ): AbstractDashboard<KubernetesClient>(
332
+ mock(),
333
+ " luke" ,
334
+ " Tatooine" ,
335
+ mock<HttpRequest >()
336
+ ) {
337
+ var connectInvoked = 0
338
+ override fun doConnect (): HttpRequest .HttpStatusCode ? {
339
+ connectInvoked++
340
+ // success
341
+ return HttpRequest .HttpStatusCode (" http://localhost" , httpStatusCode)
342
+ }
343
+ }
344
+ }
0 commit comments