@@ -1657,7 +1657,6 @@ func TestApplyInlineOverrides(t *testing.T) {
16571657 t .Run (tt .name , func (t * testing.T ) {
16581658 t .Parallel ()
16591659
1660-
16611660 wtc := & vmcpconfig.WorkloadToolConfig {Overrides : tt .existing }
16621661 toolConfig := mcpv1alpha1.WorkloadToolConfig {Overrides : tt .inline }
16631662 (& Converter {}).applyInlineOverrides (toolConfig , wtc )
@@ -1738,12 +1737,178 @@ func TestConvertToolConfigs(t *testing.T) {
17381737 }
17391738
17401739 agg := & vmcpconfig.AggregationConfig {}
1741- converter .convertToolConfigs (ctx , vmcp , agg )
1740+ err := converter .convertToolConfigs (ctx , vmcp , agg )
17421741
1742+ require .NoError (t , err )
17431743 require .Len (t , agg .Tools , 1 )
17441744 assert .Equal (t , tt .expectedWorkload , agg .Tools [0 ].Workload )
17451745 assert .Equal (t , tt .expectedFilter , agg .Tools [0 ].Filter )
17461746 assert .Equal (t , tt .expectedOverride , agg .Tools [0 ].Overrides )
17471747 })
17481748 }
17491749}
1750+
1751+ // TestConvertToolConfigs_FailClosed tests that MCPToolConfig resolution errors cause conversion to fail.
1752+ // This is a security feature: if a user explicitly references an MCPToolConfig (for tool filtering or
1753+ // security policy enforcement), we should fail rather than deploy without the intended configuration.
1754+ func TestConvertToolConfigs_FailClosed (t * testing.T ) {
1755+ t .Parallel ()
1756+
1757+ tests := []struct {
1758+ name string
1759+ tools []mcpv1alpha1.WorkloadToolConfig
1760+ existingConfig * mcpv1alpha1.MCPToolConfig
1761+ expectError bool
1762+ expectedErrMsg string
1763+ }{
1764+ {
1765+ name : "error when MCPToolConfig reference not found (fail closed)" ,
1766+ tools : []mcpv1alpha1.WorkloadToolConfig {{
1767+ Workload : "backend1" ,
1768+ ToolConfigRef : & mcpv1alpha1.ToolConfigRef {Name : "nonexistent-config" },
1769+ }},
1770+ existingConfig : nil , // MCPToolConfig doesn't exist in cluster
1771+ expectError : true ,
1772+ expectedErrMsg : "MCPToolConfig resolution failed for \" nonexistent-config\" " ,
1773+ },
1774+ {
1775+ name : "no error when no ToolConfigRef specified" ,
1776+ tools : []mcpv1alpha1.WorkloadToolConfig {{
1777+ Workload : "backend1" ,
1778+ Filter : []string {"tool1" },
1779+ }},
1780+ existingConfig : nil ,
1781+ expectError : false ,
1782+ },
1783+ {
1784+ name : "successful when MCPToolConfig exists" ,
1785+ tools : []mcpv1alpha1.WorkloadToolConfig {{
1786+ Workload : "backend1" ,
1787+ ToolConfigRef : & mcpv1alpha1.ToolConfigRef {Name : "valid-config" },
1788+ }},
1789+ existingConfig : newMCPToolConfig ("valid-config" , "default" , []string {"fetch" }, nil ),
1790+ expectError : false ,
1791+ },
1792+ }
1793+
1794+ for _ , tt := range tests {
1795+ t .Run (tt .name , func (t * testing.T ) {
1796+ t .Parallel ()
1797+
1798+ ctx := log .IntoContext (context .Background (), logr .Discard ())
1799+ var k8sClient client.Client
1800+ if tt .existingConfig != nil {
1801+ k8sClient = newTestK8sClient (t , tt .existingConfig )
1802+ } else {
1803+ k8sClient = newTestK8sClient (t )
1804+ }
1805+
1806+ converter := newTestConverter (t , newNoOpMockResolver (t ))
1807+ converter .k8sClient = k8sClient
1808+
1809+ vmcp := & mcpv1alpha1.VirtualMCPServer {
1810+ ObjectMeta : metav1.ObjectMeta {Name : "test-vmcp" , Namespace : "default" },
1811+ Spec : mcpv1alpha1.VirtualMCPServerSpec {Aggregation : & mcpv1alpha1.AggregationConfig {Tools : tt .tools }},
1812+ }
1813+
1814+ agg := & vmcpconfig.AggregationConfig {}
1815+ err := converter .convertToolConfigs (ctx , vmcp , agg )
1816+
1817+ if tt .expectError {
1818+ require .Error (t , err )
1819+ assert .Contains (t , err .Error (), tt .expectedErrMsg )
1820+ } else {
1821+ require .NoError (t , err )
1822+ }
1823+ })
1824+ }
1825+ }
1826+
1827+ // TestConvert_MCPToolConfigFailClosed tests that MCPToolConfig resolution errors propagate through
1828+ // the full Convert() method and prevent VirtualMCPServer deployment.
1829+ func TestConvert_MCPToolConfigFailClosed (t * testing.T ) {
1830+ t .Parallel ()
1831+
1832+ tests := []struct {
1833+ name string
1834+ vmcp * mcpv1alpha1.VirtualMCPServer
1835+ existingConfig * mcpv1alpha1.MCPToolConfig
1836+ expectError bool
1837+ expectedErrMsg string
1838+ }{
1839+ {
1840+ name : "Convert fails when MCPToolConfig not found" ,
1841+ vmcp : & mcpv1alpha1.VirtualMCPServer {
1842+ ObjectMeta : metav1.ObjectMeta {Name : "test-vmcp" , Namespace : "default" },
1843+ Spec : mcpv1alpha1.VirtualMCPServerSpec {
1844+ GroupRef : mcpv1alpha1.GroupRef {Name : "test-group" },
1845+ Aggregation : & mcpv1alpha1.AggregationConfig {
1846+ Tools : []mcpv1alpha1.WorkloadToolConfig {{
1847+ Workload : "backend1" ,
1848+ ToolConfigRef : & mcpv1alpha1.ToolConfigRef {Name : "missing-config" },
1849+ }},
1850+ },
1851+ },
1852+ },
1853+ existingConfig : nil ,
1854+ expectError : true ,
1855+ expectedErrMsg : "failed to convert aggregation config" ,
1856+ },
1857+ {
1858+ name : "Convert succeeds when MCPToolConfig exists" ,
1859+ vmcp : & mcpv1alpha1.VirtualMCPServer {
1860+ ObjectMeta : metav1.ObjectMeta {Name : "test-vmcp" , Namespace : "default" },
1861+ Spec : mcpv1alpha1.VirtualMCPServerSpec {
1862+ GroupRef : mcpv1alpha1.GroupRef {Name : "test-group" },
1863+ Aggregation : & mcpv1alpha1.AggregationConfig {
1864+ Tools : []mcpv1alpha1.WorkloadToolConfig {{
1865+ Workload : "backend1" ,
1866+ ToolConfigRef : & mcpv1alpha1.ToolConfigRef {Name : "valid-config" },
1867+ }},
1868+ },
1869+ },
1870+ },
1871+ existingConfig : newMCPToolConfig ("valid-config" , "default" , []string {"fetch" }, nil ),
1872+ expectError : false ,
1873+ },
1874+ {
1875+ name : "Convert succeeds when no Aggregation specified" ,
1876+ vmcp : & mcpv1alpha1.VirtualMCPServer {
1877+ ObjectMeta : metav1.ObjectMeta {Name : "test-vmcp" , Namespace : "default" },
1878+ Spec : mcpv1alpha1.VirtualMCPServerSpec {
1879+ GroupRef : mcpv1alpha1.GroupRef {Name : "test-group" },
1880+ },
1881+ },
1882+ existingConfig : nil ,
1883+ expectError : false ,
1884+ },
1885+ }
1886+
1887+ for _ , tt := range tests {
1888+ t .Run (tt .name , func (t * testing.T ) {
1889+ t .Parallel ()
1890+
1891+ ctx := log .IntoContext (context .Background (), logr .Discard ())
1892+ var k8sClient client.Client
1893+ if tt .existingConfig != nil {
1894+ k8sClient = newTestK8sClient (t , tt .existingConfig )
1895+ } else {
1896+ k8sClient = newTestK8sClient (t )
1897+ }
1898+
1899+ converter := newTestConverter (t , newNoOpMockResolver (t ))
1900+ converter .k8sClient = k8sClient
1901+
1902+ config , err := converter .Convert (ctx , tt .vmcp )
1903+
1904+ if tt .expectError {
1905+ require .Error (t , err )
1906+ assert .Contains (t , err .Error (), tt .expectedErrMsg )
1907+ assert .Nil (t , config )
1908+ } else {
1909+ require .NoError (t , err )
1910+ assert .NotNil (t , config )
1911+ }
1912+ })
1913+ }
1914+ }
0 commit comments