@@ -1365,12 +1365,178 @@ func TestConvertToolConfigs(t *testing.T) {
13651365 }
13661366
13671367 agg := & vmcpconfig.AggregationConfig {}
1368- converter .convertToolConfigs (ctx , vmcp , agg )
1368+ err := converter .convertToolConfigs (ctx , vmcp , agg )
13691369
1370+ require .NoError (t , err )
13701371 require .Len (t , agg .Tools , 1 )
13711372 assert .Equal (t , tt .expectedWorkload , agg .Tools [0 ].Workload )
13721373 assert .Equal (t , tt .expectedFilter , agg .Tools [0 ].Filter )
13731374 assert .Equal (t , tt .expectedOverride , agg .Tools [0 ].Overrides )
13741375 })
13751376 }
13761377}
1378+
1379+ // TestConvertToolConfigs_FailClosed tests that MCPToolConfig resolution errors cause conversion to fail.
1380+ // This is a security feature: if a user explicitly references an MCPToolConfig (for tool filtering or
1381+ // security policy enforcement), we should fail rather than deploy without the intended configuration.
1382+ func TestConvertToolConfigs_FailClosed (t * testing.T ) {
1383+ t .Parallel ()
1384+
1385+ tests := []struct {
1386+ name string
1387+ tools []mcpv1alpha1.WorkloadToolConfig
1388+ existingConfig * mcpv1alpha1.MCPToolConfig
1389+ expectError bool
1390+ expectedErrMsg string
1391+ }{
1392+ {
1393+ name : "error when MCPToolConfig reference not found (fail closed)" ,
1394+ tools : []mcpv1alpha1.WorkloadToolConfig {{
1395+ Workload : "backend1" ,
1396+ ToolConfigRef : & mcpv1alpha1.ToolConfigRef {Name : "nonexistent-config" },
1397+ }},
1398+ existingConfig : nil , // MCPToolConfig doesn't exist in cluster
1399+ expectError : true ,
1400+ expectedErrMsg : "MCPToolConfig resolution failed for \" nonexistent-config\" " ,
1401+ },
1402+ {
1403+ name : "no error when no ToolConfigRef specified" ,
1404+ tools : []mcpv1alpha1.WorkloadToolConfig {{
1405+ Workload : "backend1" ,
1406+ Filter : []string {"tool1" },
1407+ }},
1408+ existingConfig : nil ,
1409+ expectError : false ,
1410+ },
1411+ {
1412+ name : "successful when MCPToolConfig exists" ,
1413+ tools : []mcpv1alpha1.WorkloadToolConfig {{
1414+ Workload : "backend1" ,
1415+ ToolConfigRef : & mcpv1alpha1.ToolConfigRef {Name : "valid-config" },
1416+ }},
1417+ existingConfig : newMCPToolConfig ("valid-config" , "default" , []string {"fetch" }, nil ),
1418+ expectError : false ,
1419+ },
1420+ }
1421+
1422+ for _ , tt := range tests {
1423+ t .Run (tt .name , func (t * testing.T ) {
1424+ t .Parallel ()
1425+
1426+ ctx := log .IntoContext (context .Background (), logr .Discard ())
1427+ var k8sClient client.Client
1428+ if tt .existingConfig != nil {
1429+ k8sClient = newTestK8sClient (t , tt .existingConfig )
1430+ } else {
1431+ k8sClient = newTestK8sClient (t )
1432+ }
1433+
1434+ converter := newTestConverter (t , newNoOpMockResolver (t ))
1435+ converter .k8sClient = k8sClient
1436+
1437+ vmcp := & mcpv1alpha1.VirtualMCPServer {
1438+ ObjectMeta : metav1.ObjectMeta {Name : "test-vmcp" , Namespace : "default" },
1439+ Spec : mcpv1alpha1.VirtualMCPServerSpec {Aggregation : & mcpv1alpha1.AggregationConfig {Tools : tt .tools }},
1440+ }
1441+
1442+ agg := & vmcpconfig.AggregationConfig {}
1443+ err := converter .convertToolConfigs (ctx , vmcp , agg )
1444+
1445+ if tt .expectError {
1446+ require .Error (t , err )
1447+ assert .Contains (t , err .Error (), tt .expectedErrMsg )
1448+ } else {
1449+ require .NoError (t , err )
1450+ }
1451+ })
1452+ }
1453+ }
1454+
1455+ // TestConvert_MCPToolConfigFailClosed tests that MCPToolConfig resolution errors propagate through
1456+ // the full Convert() method and prevent VirtualMCPServer deployment.
1457+ func TestConvert_MCPToolConfigFailClosed (t * testing.T ) {
1458+ t .Parallel ()
1459+
1460+ tests := []struct {
1461+ name string
1462+ vmcp * mcpv1alpha1.VirtualMCPServer
1463+ existingConfig * mcpv1alpha1.MCPToolConfig
1464+ expectError bool
1465+ expectedErrMsg string
1466+ }{
1467+ {
1468+ name : "Convert fails when MCPToolConfig not found" ,
1469+ vmcp : & mcpv1alpha1.VirtualMCPServer {
1470+ ObjectMeta : metav1.ObjectMeta {Name : "test-vmcp" , Namespace : "default" },
1471+ Spec : mcpv1alpha1.VirtualMCPServerSpec {
1472+ GroupRef : mcpv1alpha1.GroupRef {Name : "test-group" },
1473+ Aggregation : & mcpv1alpha1.AggregationConfig {
1474+ Tools : []mcpv1alpha1.WorkloadToolConfig {{
1475+ Workload : "backend1" ,
1476+ ToolConfigRef : & mcpv1alpha1.ToolConfigRef {Name : "missing-config" },
1477+ }},
1478+ },
1479+ },
1480+ },
1481+ existingConfig : nil ,
1482+ expectError : true ,
1483+ expectedErrMsg : "failed to convert aggregation config" ,
1484+ },
1485+ {
1486+ name : "Convert succeeds when MCPToolConfig exists" ,
1487+ vmcp : & mcpv1alpha1.VirtualMCPServer {
1488+ ObjectMeta : metav1.ObjectMeta {Name : "test-vmcp" , Namespace : "default" },
1489+ Spec : mcpv1alpha1.VirtualMCPServerSpec {
1490+ GroupRef : mcpv1alpha1.GroupRef {Name : "test-group" },
1491+ Aggregation : & mcpv1alpha1.AggregationConfig {
1492+ Tools : []mcpv1alpha1.WorkloadToolConfig {{
1493+ Workload : "backend1" ,
1494+ ToolConfigRef : & mcpv1alpha1.ToolConfigRef {Name : "valid-config" },
1495+ }},
1496+ },
1497+ },
1498+ },
1499+ existingConfig : newMCPToolConfig ("valid-config" , "default" , []string {"fetch" }, nil ),
1500+ expectError : false ,
1501+ },
1502+ {
1503+ name : "Convert succeeds when no Aggregation specified" ,
1504+ vmcp : & mcpv1alpha1.VirtualMCPServer {
1505+ ObjectMeta : metav1.ObjectMeta {Name : "test-vmcp" , Namespace : "default" },
1506+ Spec : mcpv1alpha1.VirtualMCPServerSpec {
1507+ GroupRef : mcpv1alpha1.GroupRef {Name : "test-group" },
1508+ },
1509+ },
1510+ existingConfig : nil ,
1511+ expectError : false ,
1512+ },
1513+ }
1514+
1515+ for _ , tt := range tests {
1516+ t .Run (tt .name , func (t * testing.T ) {
1517+ t .Parallel ()
1518+
1519+ ctx := log .IntoContext (context .Background (), logr .Discard ())
1520+ var k8sClient client.Client
1521+ if tt .existingConfig != nil {
1522+ k8sClient = newTestK8sClient (t , tt .existingConfig )
1523+ } else {
1524+ k8sClient = newTestK8sClient (t )
1525+ }
1526+
1527+ converter := newTestConverter (t , newNoOpMockResolver (t ))
1528+ converter .k8sClient = k8sClient
1529+
1530+ config , err := converter .Convert (ctx , tt .vmcp )
1531+
1532+ if tt .expectError {
1533+ require .Error (t , err )
1534+ assert .Contains (t , err .Error (), tt .expectedErrMsg )
1535+ assert .Nil (t , config )
1536+ } else {
1537+ require .NoError (t , err )
1538+ assert .NotNil (t , config )
1539+ }
1540+ })
1541+ }
1542+ }
0 commit comments