kubectl-mcp-server 1.19.2__py3-none-any.whl → 1.21.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tests/test_vind.py ADDED
@@ -0,0 +1,512 @@
1
+ """
2
+ Unit tests for vind (vCluster in Docker) tools.
3
+
4
+ This module tests the vCluster management toolset.
5
+ """
6
+
7
+ import pytest
8
+ import json
9
+ from unittest.mock import patch, MagicMock
10
+ import subprocess
11
+
12
+
13
+ class TestVindHelpers:
14
+ """Tests for vind helper functions."""
15
+
16
+ @pytest.mark.unit
17
+ def test_vind_module_imports(self):
18
+ """Test that vind module can be imported."""
19
+ from kubectl_mcp_tool.tools.vind import (
20
+ register_vind_tools,
21
+ _vcluster_available,
22
+ _get_vcluster_version,
23
+ _run_vcluster,
24
+ vind_detect,
25
+ vind_list_clusters,
26
+ vind_status,
27
+ vind_get_kubeconfig,
28
+ vind_logs,
29
+ vind_create_cluster,
30
+ vind_delete_cluster,
31
+ vind_pause,
32
+ vind_resume,
33
+ vind_connect,
34
+ vind_disconnect,
35
+ vind_upgrade,
36
+ vind_describe,
37
+ vind_platform_start,
38
+ )
39
+ assert callable(register_vind_tools)
40
+ assert callable(_vcluster_available)
41
+ assert callable(_get_vcluster_version)
42
+ assert callable(_run_vcluster)
43
+ assert callable(vind_detect)
44
+ assert callable(vind_list_clusters)
45
+ assert callable(vind_status)
46
+ assert callable(vind_get_kubeconfig)
47
+ assert callable(vind_logs)
48
+ assert callable(vind_create_cluster)
49
+ assert callable(vind_delete_cluster)
50
+ assert callable(vind_pause)
51
+ assert callable(vind_resume)
52
+ assert callable(vind_connect)
53
+ assert callable(vind_disconnect)
54
+ assert callable(vind_upgrade)
55
+ assert callable(vind_describe)
56
+ assert callable(vind_platform_start)
57
+
58
+ @pytest.mark.unit
59
+ def test_vcluster_available_when_installed(self):
60
+ """Test _vcluster_available returns True when CLI is installed."""
61
+ from kubectl_mcp_tool.tools.vind import _vcluster_available
62
+
63
+ with patch("subprocess.run") as mock_run:
64
+ mock_run.return_value = MagicMock(returncode=0)
65
+ result = _vcluster_available()
66
+ assert result is True
67
+
68
+ @pytest.mark.unit
69
+ def test_vcluster_available_when_not_installed(self):
70
+ """Test _vcluster_available returns False when CLI is not installed."""
71
+ from kubectl_mcp_tool.tools.vind import _vcluster_available
72
+
73
+ with patch("subprocess.run") as mock_run:
74
+ mock_run.side_effect = FileNotFoundError()
75
+ result = _vcluster_available()
76
+ assert result is False
77
+
78
+ @pytest.mark.unit
79
+ def test_get_vcluster_version(self):
80
+ """Test _get_vcluster_version extracts version correctly."""
81
+ from kubectl_mcp_tool.tools.vind import _get_vcluster_version
82
+
83
+ with patch("subprocess.run") as mock_run:
84
+ mock_run.return_value = MagicMock(
85
+ returncode=0,
86
+ stdout="vcluster version v0.19.0"
87
+ )
88
+ result = _get_vcluster_version()
89
+ assert result == "v0.19.0"
90
+
91
+ @pytest.mark.unit
92
+ def test_get_vcluster_version_not_installed(self):
93
+ """Test _get_vcluster_version returns None when not installed."""
94
+ from kubectl_mcp_tool.tools.vind import _get_vcluster_version
95
+
96
+ with patch("subprocess.run") as mock_run:
97
+ mock_run.side_effect = FileNotFoundError()
98
+ result = _get_vcluster_version()
99
+ assert result is None
100
+
101
+ @pytest.mark.unit
102
+ def test_run_vcluster_not_available(self):
103
+ """Test _run_vcluster returns error when CLI not available."""
104
+ from kubectl_mcp_tool.tools.vind import _run_vcluster
105
+
106
+ with patch("subprocess.run") as mock_run:
107
+ mock_run.side_effect = FileNotFoundError()
108
+ result = _run_vcluster(["list"])
109
+ assert result["success"] is False
110
+ assert "not available" in result["error"]
111
+
112
+ @pytest.mark.unit
113
+ def test_run_vcluster_success(self):
114
+ """Test _run_vcluster returns success on successful command."""
115
+ from kubectl_mcp_tool.tools.vind import _run_vcluster
116
+
117
+ with patch("subprocess.run") as mock_run:
118
+ # First call for availability check
119
+ mock_run.return_value = MagicMock(
120
+ returncode=0,
121
+ stdout="test output",
122
+ stderr=""
123
+ )
124
+ result = _run_vcluster(["list"])
125
+ assert result["success"] is True
126
+ assert result["output"] == "test output"
127
+
128
+ @pytest.mark.unit
129
+ def test_run_vcluster_timeout(self):
130
+ """Test _run_vcluster handles timeout."""
131
+ from kubectl_mcp_tool.tools.vind import _run_vcluster
132
+
133
+ with patch("subprocess.run") as mock_run:
134
+ # First call succeeds (availability check), second times out
135
+ mock_run.side_effect = [
136
+ MagicMock(returncode=0), # availability check
137
+ subprocess.TimeoutExpired(cmd="vcluster", timeout=120)
138
+ ]
139
+ result = _run_vcluster(["create", "test"])
140
+ assert result["success"] is False
141
+ assert "timed out" in result["error"]
142
+
143
+ @pytest.mark.unit
144
+ def test_run_vcluster_with_json_output(self):
145
+ """Test _run_vcluster parses JSON output."""
146
+ from kubectl_mcp_tool.tools.vind import _run_vcluster
147
+
148
+ with patch("subprocess.run") as mock_run:
149
+ mock_run.return_value = MagicMock(
150
+ returncode=0,
151
+ stdout='[{"Name": "test", "Status": "Running"}]',
152
+ stderr=""
153
+ )
154
+ result = _run_vcluster(["list"], json_output=True)
155
+ assert result["success"] is True
156
+ assert result["data"] == [{"Name": "test", "Status": "Running"}]
157
+
158
+
159
+ class TestVindDetect:
160
+ """Tests for vind_detect function."""
161
+
162
+ @pytest.mark.unit
163
+ def test_vind_detect_installed(self):
164
+ """Test vind_detect when vcluster is installed."""
165
+ from kubectl_mcp_tool.tools.vind import vind_detect
166
+
167
+ with patch("subprocess.run") as mock_run:
168
+ mock_run.return_value = MagicMock(
169
+ returncode=0,
170
+ stdout="vcluster version v0.19.0"
171
+ )
172
+ result = vind_detect()
173
+ assert result["installed"] is True
174
+ assert result["cli_available"] is True
175
+ assert result["version"] == "v0.19.0"
176
+ assert result["install_instructions"] is None
177
+
178
+ @pytest.mark.unit
179
+ def test_vind_detect_not_installed(self):
180
+ """Test vind_detect when vcluster is not installed."""
181
+ from kubectl_mcp_tool.tools.vind import vind_detect
182
+
183
+ with patch("subprocess.run") as mock_run:
184
+ mock_run.side_effect = FileNotFoundError()
185
+ result = vind_detect()
186
+ assert result["installed"] is False
187
+ assert result["cli_available"] is False
188
+ assert result["version"] is None
189
+ assert result["install_instructions"] is not None
190
+
191
+
192
+ class TestVindListClusters:
193
+ """Tests for vind_list_clusters function."""
194
+
195
+ @pytest.mark.unit
196
+ def test_vind_list_clusters_success(self):
197
+ """Test vind_list_clusters returns cluster list."""
198
+ from kubectl_mcp_tool.tools.vind import vind_list_clusters
199
+
200
+ mock_clusters = [
201
+ {
202
+ "Name": "dev-cluster",
203
+ "Namespace": "vcluster-dev",
204
+ "Status": "Running",
205
+ "Version": "v1.29.0",
206
+ "Connected": True,
207
+ "Created": "2024-01-01T00:00:00Z",
208
+ "Age": "1d"
209
+ }
210
+ ]
211
+
212
+ with patch("kubectl_mcp_tool.tools.vind._vcluster_available", return_value=True):
213
+ with patch("kubectl_mcp_tool.tools.vind.subprocess.run") as mock_run:
214
+ mock_run.return_value = MagicMock(
215
+ returncode=0,
216
+ stdout=json.dumps(mock_clusters),
217
+ stderr=""
218
+ )
219
+ result = vind_list_clusters()
220
+ assert result["success"] is True
221
+ assert result["total"] == 1
222
+ assert len(result["clusters"]) == 1
223
+ assert result["clusters"][0]["name"] == "dev-cluster"
224
+
225
+ @pytest.mark.unit
226
+ def test_vind_list_clusters_empty(self):
227
+ """Test vind_list_clusters returns empty list."""
228
+ from kubectl_mcp_tool.tools.vind import vind_list_clusters
229
+
230
+ with patch("kubectl_mcp_tool.tools.vind._vcluster_available", return_value=True):
231
+ with patch("kubectl_mcp_tool.tools.vind.subprocess.run") as mock_run:
232
+ mock_run.return_value = MagicMock(
233
+ returncode=0,
234
+ stdout="[]",
235
+ stderr=""
236
+ )
237
+ result = vind_list_clusters()
238
+ assert result["success"] is True
239
+ assert result["total"] == 0
240
+
241
+
242
+ class TestVindCreateCluster:
243
+ """Tests for vind_create_cluster function."""
244
+
245
+ @pytest.mark.unit
246
+ def test_vind_create_cluster_basic(self):
247
+ """Test vind_create_cluster with basic options."""
248
+ from kubectl_mcp_tool.tools.vind import vind_create_cluster
249
+
250
+ with patch("subprocess.run") as mock_run:
251
+ mock_run.return_value = MagicMock(
252
+ returncode=0,
253
+ stdout="vCluster 'test' created successfully",
254
+ stderr=""
255
+ )
256
+ result = vind_create_cluster(name="test")
257
+ assert result["success"] is True
258
+ assert "created" in result["message"].lower()
259
+
260
+ @pytest.mark.unit
261
+ def test_vind_create_cluster_with_options(self):
262
+ """Test vind_create_cluster with all options."""
263
+ from kubectl_mcp_tool.tools.vind import vind_create_cluster
264
+
265
+ with patch("subprocess.run") as mock_run:
266
+ mock_run.return_value = MagicMock(
267
+ returncode=0,
268
+ stdout="vCluster 'prod' created successfully",
269
+ stderr=""
270
+ )
271
+ result = vind_create_cluster(
272
+ name="prod",
273
+ namespace="production",
274
+ kubernetes_version="v1.29.0",
275
+ set_values=["sync.toHost.pods.enabled=true"],
276
+ connect=True
277
+ )
278
+ assert result["success"] is True
279
+
280
+
281
+ class TestVindPauseResume:
282
+ """Tests for vind_pause and vind_resume functions."""
283
+
284
+ @pytest.mark.unit
285
+ def test_vind_pause_success(self):
286
+ """Test vind_pause pauses cluster."""
287
+ from kubectl_mcp_tool.tools.vind import vind_pause
288
+
289
+ with patch("subprocess.run") as mock_run:
290
+ mock_run.return_value = MagicMock(
291
+ returncode=0,
292
+ stdout="vCluster 'test' paused",
293
+ stderr=""
294
+ )
295
+ result = vind_pause(name="test")
296
+ assert result["success"] is True
297
+ assert "paused" in result["message"].lower()
298
+
299
+ @pytest.mark.unit
300
+ def test_vind_resume_success(self):
301
+ """Test vind_resume resumes cluster."""
302
+ from kubectl_mcp_tool.tools.vind import vind_resume
303
+
304
+ with patch("subprocess.run") as mock_run:
305
+ mock_run.return_value = MagicMock(
306
+ returncode=0,
307
+ stdout="vCluster 'test' resumed",
308
+ stderr=""
309
+ )
310
+ result = vind_resume(name="test")
311
+ assert result["success"] is True
312
+ assert "resumed" in result["message"].lower()
313
+
314
+
315
+ class TestVindConnect:
316
+ """Tests for vind_connect and vind_disconnect functions."""
317
+
318
+ @pytest.mark.unit
319
+ def test_vind_connect_success(self):
320
+ """Test vind_connect connects to cluster."""
321
+ from kubectl_mcp_tool.tools.vind import vind_connect
322
+
323
+ with patch("subprocess.run") as mock_run:
324
+ mock_run.return_value = MagicMock(
325
+ returncode=0,
326
+ stdout="Connected to vCluster 'test'",
327
+ stderr=""
328
+ )
329
+ result = vind_connect(name="test")
330
+ assert result["success"] is True
331
+ assert "connected" in result["message"].lower()
332
+
333
+ @pytest.mark.unit
334
+ def test_vind_disconnect_success(self):
335
+ """Test vind_disconnect disconnects from cluster."""
336
+ from kubectl_mcp_tool.tools.vind import vind_disconnect
337
+
338
+ with patch("subprocess.run") as mock_run:
339
+ mock_run.return_value = MagicMock(
340
+ returncode=0,
341
+ stdout="Disconnected",
342
+ stderr=""
343
+ )
344
+ result = vind_disconnect("", "")
345
+ assert result["success"] is True
346
+
347
+
348
+ class TestVindToolsRegistration:
349
+ """Tests for vind tools registration."""
350
+
351
+ @pytest.mark.unit
352
+ def test_vind_tools_import(self):
353
+ """Test that vind tools can be imported."""
354
+ from kubectl_mcp_tool.tools.vind import register_vind_tools
355
+ assert callable(register_vind_tools)
356
+
357
+ @pytest.mark.unit
358
+ @pytest.mark.asyncio
359
+ async def test_vind_tools_register(self, mock_all_kubernetes_apis):
360
+ """Test that vind tools register correctly."""
361
+ from kubectl_mcp_tool.mcp_server import MCPServer
362
+
363
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
364
+ server = MCPServer(name="test")
365
+
366
+ tools = await server.server.list_tools()
367
+ tool_names = {t.name for t in tools}
368
+
369
+ vind_tools = [
370
+ "vind_detect_tool",
371
+ "vind_list_clusters_tool",
372
+ "vind_status_tool",
373
+ "vind_get_kubeconfig_tool",
374
+ "vind_logs_tool",
375
+ "vind_create_cluster_tool",
376
+ "vind_delete_cluster_tool",
377
+ "vind_pause_tool",
378
+ "vind_resume_tool",
379
+ "vind_connect_tool",
380
+ "vind_disconnect_tool",
381
+ "vind_upgrade_tool",
382
+ "vind_describe_tool",
383
+ "vind_platform_start_tool",
384
+ ]
385
+ for tool in vind_tools:
386
+ assert tool in tool_names, f"vind tool '{tool}' not registered"
387
+
388
+ @pytest.mark.unit
389
+ @pytest.mark.asyncio
390
+ async def test_vind_tool_count(self, mock_all_kubernetes_apis):
391
+ """Test that correct number of vind tools are registered."""
392
+ from kubectl_mcp_tool.mcp_server import MCPServer
393
+
394
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
395
+ server = MCPServer(name="test")
396
+
397
+ tools = await server.server.list_tools()
398
+ tool_names = {t.name for t in tools}
399
+ vind_tools = [name for name in tool_names if name.startswith("vind_")]
400
+ assert len(vind_tools) == 14, f"Expected 14 vind tools, got {len(vind_tools)}: {vind_tools}"
401
+
402
+ @pytest.mark.unit
403
+ def test_vind_non_destructive_mode(self, mock_all_kubernetes_apis):
404
+ """Test that vind write operations are blocked in non-destructive mode."""
405
+ from kubectl_mcp_tool.mcp_server import MCPServer
406
+
407
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
408
+ server = MCPServer(name="test", disable_destructive=True)
409
+
410
+ assert server.non_destructive is True
411
+
412
+ @pytest.mark.unit
413
+ @pytest.mark.asyncio
414
+ async def test_vind_tools_have_descriptions(self, mock_all_kubernetes_apis):
415
+ """Test that all vind tools have descriptions."""
416
+ from kubectl_mcp_tool.mcp_server import MCPServer
417
+
418
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
419
+ server = MCPServer(name="test")
420
+
421
+ tools = await server.server.list_tools()
422
+ vind_tools = [t for t in tools if t.name.startswith("vind_")]
423
+ tools_without_description = [
424
+ t.name for t in vind_tools
425
+ if not t.description or len(t.description.strip()) == 0
426
+ ]
427
+ assert not tools_without_description, f"vind tools without descriptions: {tools_without_description}"
428
+
429
+
430
+ class TestVindNonDestructiveBlocking:
431
+ """Tests for non-destructive mode blocking of vind write operations."""
432
+
433
+ @pytest.mark.unit
434
+ @pytest.mark.asyncio
435
+ async def test_create_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
436
+ """Test that vind_create_cluster_tool is blocked in non-destructive mode."""
437
+ from kubectl_mcp_tool.tools.vind import register_vind_tools
438
+
439
+ try:
440
+ from fastmcp import FastMCP
441
+ except ImportError:
442
+ from mcp.server.fastmcp import FastMCP
443
+
444
+ mcp = FastMCP(name="test")
445
+ register_vind_tools(mcp, non_destructive=True)
446
+
447
+ tool = await mcp.get_tool("vind_create_cluster_tool")
448
+ result = tool.fn(name="test")
449
+ result_dict = json.loads(result)
450
+ assert result_dict["success"] is False
451
+ assert "non-destructive" in result_dict["error"].lower()
452
+
453
+ @pytest.mark.unit
454
+ @pytest.mark.asyncio
455
+ async def test_delete_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
456
+ """Test that vind_delete_cluster_tool is blocked in non-destructive mode."""
457
+ from kubectl_mcp_tool.tools.vind import register_vind_tools
458
+
459
+ try:
460
+ from fastmcp import FastMCP
461
+ except ImportError:
462
+ from mcp.server.fastmcp import FastMCP
463
+
464
+ mcp = FastMCP(name="test")
465
+ register_vind_tools(mcp, non_destructive=True)
466
+
467
+ tool = await mcp.get_tool("vind_delete_cluster_tool")
468
+ result = tool.fn(name="test")
469
+ result_dict = json.loads(result)
470
+ assert result_dict["success"] is False
471
+ assert "non-destructive" in result_dict["error"].lower()
472
+
473
+ @pytest.mark.unit
474
+ @pytest.mark.asyncio
475
+ async def test_pause_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
476
+ """Test that vind_pause_tool is blocked in non-destructive mode."""
477
+ from kubectl_mcp_tool.tools.vind import register_vind_tools
478
+
479
+ try:
480
+ from fastmcp import FastMCP
481
+ except ImportError:
482
+ from mcp.server.fastmcp import FastMCP
483
+
484
+ mcp = FastMCP(name="test")
485
+ register_vind_tools(mcp, non_destructive=True)
486
+
487
+ tool = await mcp.get_tool("vind_pause_tool")
488
+ result = tool.fn(name="test")
489
+ result_dict = json.loads(result)
490
+ assert result_dict["success"] is False
491
+ assert "non-destructive" in result_dict["error"].lower()
492
+
493
+ @pytest.mark.unit
494
+ @pytest.mark.asyncio
495
+ async def test_read_operations_allowed_in_non_destructive(self, mock_all_kubernetes_apis):
496
+ """Test that read operations work in non-destructive mode."""
497
+ from kubectl_mcp_tool.tools.vind import register_vind_tools
498
+
499
+ try:
500
+ from fastmcp import FastMCP
501
+ except ImportError:
502
+ from mcp.server.fastmcp import FastMCP
503
+
504
+ mcp = FastMCP(name="test")
505
+ register_vind_tools(mcp, non_destructive=True)
506
+
507
+ tool = await mcp.get_tool("vind_detect_tool")
508
+ with patch("subprocess.run") as mock_run:
509
+ mock_run.side_effect = FileNotFoundError()
510
+ result = tool.fn()
511
+ result_dict = json.loads(result)
512
+ assert "installed" in result_dict