1+ import base64
2+ from unittest .mock import AsyncMock , MagicMock
13
2- from kubernetes_asyncio .client .models import V1ObjectMeta
4+ import pytest
5+ from kubernetes_asyncio .client .exceptions import ApiException
6+ from kubernetes_asyncio .client .models import V1ConfigMap , V1ObjectMeta , V1Secret
37
48from jumpstarter_kubernetes import V1Alpha1Client , V1Alpha1ClientStatus
9+ from jumpstarter_kubernetes .clients import ClientsV1Alpha1Api
510
611TEST_CLIENT = V1Alpha1Client (
712 api_version = "jumpstarter.dev/v1alpha1" ,
@@ -123,7 +128,6 @@ def test_client_from_dict_without_status():
123128
124129def test_client_rich_add_columns ():
125130 """Test V1Alpha1Client.rich_add_columns"""
126- from unittest .mock import MagicMock
127131
128132 mock_table = MagicMock ()
129133 V1Alpha1Client .rich_add_columns (mock_table )
@@ -134,7 +138,6 @@ def test_client_rich_add_columns():
134138
135139def test_client_rich_add_rows_with_status ():
136140 """Test V1Alpha1Client.rich_add_rows with status"""
137- from unittest .mock import MagicMock
138141
139142 mock_table = MagicMock ()
140143 TEST_CLIENT .rich_add_rows (mock_table )
@@ -143,7 +146,6 @@ def test_client_rich_add_rows_with_status():
143146
144147def test_client_rich_add_rows_without_status ():
145148 """Test V1Alpha1Client.rich_add_rows without status"""
146- from unittest .mock import MagicMock
147149
148150 client = V1Alpha1Client (
149151 api_version = "jumpstarter.dev/v1alpha1" ,
@@ -205,7 +207,6 @@ def test_client_list_from_dict():
205207
206208def test_client_list_rich_add_columns ():
207209 """Test V1Alpha1ClientList.rich_add_columns"""
208- from unittest .mock import MagicMock
209210
210211 from jumpstarter_kubernetes import V1Alpha1ClientList
211212
@@ -216,7 +217,6 @@ def test_client_list_rich_add_columns():
216217
217218def test_client_list_rich_add_rows ():
218219 """Test V1Alpha1ClientList.rich_add_rows"""
219- from unittest .mock import MagicMock
220220
221221 from jumpstarter_kubernetes import V1Alpha1ClientList
222222
@@ -234,3 +234,179 @@ def test_client_list_rich_add_names():
234234 names = []
235235 client_list .rich_add_names (names )
236236 assert names == ["client.jumpstarter.dev/test-client" ]
237+
238+
239+ # Tests for get_ca_bundle and get_client_config
240+
241+
242+ @pytest .mark .asyncio
243+ async def test_get_ca_bundle_with_ca_cert ():
244+ """Test get_ca_bundle returns base64-encoded CA certificate"""
245+ api = ClientsV1Alpha1Api (namespace = "test-namespace" )
246+ api .core_api = AsyncMock ()
247+
248+ # Mock ConfigMap with CA certificate
249+ ca_cert_pem = "-----BEGIN CERTIFICATE-----\n MIIBtest\n -----END CERTIFICATE-----"
250+ mock_configmap = V1ConfigMap (data = {"ca.crt" : ca_cert_pem })
251+ api .core_api .read_namespaced_config_map = AsyncMock (return_value = mock_configmap )
252+
253+ result = await api .get_ca_bundle ()
254+
255+ # Verify it's base64-encoded
256+ expected = base64 .b64encode (ca_cert_pem .encode ("utf-8" )).decode ("utf-8" )
257+ assert result == expected
258+ api .core_api .read_namespaced_config_map .assert_called_once_with (
259+ "jumpstarter-service-ca-cert" , "test-namespace"
260+ )
261+
262+
263+ @pytest .mark .asyncio
264+ async def test_get_ca_bundle_empty_ca_cert ():
265+ """Test get_ca_bundle returns empty string when ca.crt is empty"""
266+ api = ClientsV1Alpha1Api (namespace = "test-namespace" )
267+ api .core_api = AsyncMock ()
268+
269+ # Mock ConfigMap with empty CA certificate
270+ mock_configmap = V1ConfigMap (data = {"ca.crt" : "" })
271+ api .core_api .read_namespaced_config_map = AsyncMock (return_value = mock_configmap )
272+
273+ result = await api .get_ca_bundle ()
274+
275+ assert result == ""
276+
277+
278+ @pytest .mark .asyncio
279+ async def test_get_ca_bundle_missing_ca_crt_key ():
280+ """Test get_ca_bundle returns empty string when ca.crt key is missing"""
281+ api = ClientsV1Alpha1Api (namespace = "test-namespace" )
282+ api .core_api = AsyncMock ()
283+
284+ # Mock ConfigMap without ca.crt key
285+ mock_configmap = V1ConfigMap (data = {"other-key" : "value" })
286+ api .core_api .read_namespaced_config_map = AsyncMock (return_value = mock_configmap )
287+
288+ result = await api .get_ca_bundle ()
289+
290+ assert result == ""
291+
292+
293+ @pytest .mark .asyncio
294+ async def test_get_ca_bundle_configmap_not_found ():
295+ """Test get_ca_bundle returns empty string when ConfigMap doesn't exist"""
296+ api = ClientsV1Alpha1Api (namespace = "test-namespace" )
297+ api .core_api = AsyncMock ()
298+
299+ # Mock 404 error
300+ api .core_api .read_namespaced_config_map = AsyncMock (
301+ side_effect = ApiException (status = 404 , reason = "Not Found" )
302+ )
303+
304+ result = await api .get_ca_bundle ()
305+
306+ assert result == ""
307+
308+
309+ @pytest .mark .asyncio
310+ async def test_get_ca_bundle_other_api_error ():
311+ """Test get_ca_bundle raises exception for non-404 errors"""
312+ api = ClientsV1Alpha1Api (namespace = "test-namespace" )
313+ api .core_api = AsyncMock ()
314+
315+ # Mock 403 error
316+ api .core_api .read_namespaced_config_map = AsyncMock (
317+ side_effect = ApiException (status = 403 , reason = "Forbidden" )
318+ )
319+
320+ with pytest .raises (ApiException ) as exc_info :
321+ await api .get_ca_bundle ()
322+
323+ assert exc_info .value .status == 403
324+
325+
326+ @pytest .mark .asyncio
327+ async def test_get_client_config_includes_ca_bundle ():
328+ """Test get_client_config includes CA bundle from ConfigMap"""
329+ api = ClientsV1Alpha1Api (namespace = "test-namespace" )
330+ api .api = AsyncMock ()
331+ api .core_api = AsyncMock ()
332+
333+ # Mock client response
334+ client_dict = {
335+ "apiVersion" : "jumpstarter.dev/v1alpha1" ,
336+ "kind" : "Client" ,
337+ "metadata" : {
338+ "creationTimestamp" : "2021-10-01T00:00:00Z" ,
339+ "generation" : 1 ,
340+ "name" : "test-client" ,
341+ "namespace" : "test-namespace" ,
342+ "resourceVersion" : "1" ,
343+ "uid" : "test-uid" ,
344+ },
345+ "status" : {
346+ "credential" : {"name" : "test-secret" },
347+ "endpoint" : "https://test-endpoint:8082" ,
348+ },
349+ }
350+ api .api .get_namespaced_custom_object = AsyncMock (return_value = client_dict )
351+
352+ # Mock secret with token
353+ token = "test-token-value"
354+ mock_secret = V1Secret (data = {"token" : base64 .b64encode (token .encode ()).decode ()})
355+ api .core_api .read_namespaced_secret = AsyncMock (return_value = mock_secret )
356+
357+ # Mock ConfigMap with CA certificate
358+ ca_cert_pem = "-----BEGIN CERTIFICATE-----\n MIIBtest\n -----END CERTIFICATE-----"
359+ mock_configmap = V1ConfigMap (data = {"ca.crt" : ca_cert_pem })
360+ api .core_api .read_namespaced_config_map = AsyncMock (return_value = mock_configmap )
361+
362+ config = await api .get_client_config ("test-client" , allow = [], unsafe = False )
363+
364+ # Verify CA bundle is included and base64-encoded
365+ expected_ca = base64 .b64encode (ca_cert_pem .encode ("utf-8" )).decode ("utf-8" )
366+ assert config .tls .ca == expected_ca
367+ assert config .endpoint == "https://test-endpoint:8082"
368+ assert config .token == token
369+
370+
371+ @pytest .mark .asyncio
372+ async def test_get_client_config_without_ca_bundle ():
373+ """Test get_client_config works when CA ConfigMap doesn't exist"""
374+ api = ClientsV1Alpha1Api (namespace = "test-namespace" )
375+ api .api = AsyncMock ()
376+ api .core_api = AsyncMock ()
377+
378+ # Mock client response
379+ client_dict = {
380+ "apiVersion" : "jumpstarter.dev/v1alpha1" ,
381+ "kind" : "Client" ,
382+ "metadata" : {
383+ "creationTimestamp" : "2021-10-01T00:00:00Z" ,
384+ "generation" : 1 ,
385+ "name" : "test-client" ,
386+ "namespace" : "test-namespace" ,
387+ "resourceVersion" : "1" ,
388+ "uid" : "test-uid" ,
389+ },
390+ "status" : {
391+ "credential" : {"name" : "test-secret" },
392+ "endpoint" : "https://test-endpoint:8082" ,
393+ },
394+ }
395+ api .api .get_namespaced_custom_object = AsyncMock (return_value = client_dict )
396+
397+ # Mock secret with token
398+ token = "test-token-value"
399+ mock_secret = V1Secret (data = {"token" : base64 .b64encode (token .encode ()).decode ()})
400+ api .core_api .read_namespaced_secret = AsyncMock (return_value = mock_secret )
401+
402+ # Mock ConfigMap not found
403+ api .core_api .read_namespaced_config_map = AsyncMock (
404+ side_effect = ApiException (status = 404 , reason = "Not Found" )
405+ )
406+
407+ config = await api .get_client_config ("test-client" , allow = [], unsafe = False )
408+
409+ # Verify CA bundle is empty
410+ assert config .tls .ca == ""
411+ assert config .endpoint == "https://test-endpoint:8082"
412+ assert config .token == token
0 commit comments