@@ -1323,6 +1323,342 @@ async fn test_default_patterns_empty() {
13231323 assert ! ( config. dlp. patterns. is_empty( ) ) ;
13241324}
13251325
1326+ // ========== Config Error & Edge-Case Tests ==========
1327+
1328+ #[ tokio:: test]
1329+ async fn test_config_invalid_toml_syntax ( ) {
1330+ let toml_str = r#"
1331+ [server
1332+ port = 8080
1333+ "# ;
1334+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1335+ }
1336+
1337+ #[ tokio:: test]
1338+ async fn test_config_unclosed_string ( ) {
1339+ let toml_str = r#"
1340+ [server]
1341+ host = "0.0.0.0
1342+ [upstream]
1343+ "# ;
1344+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1345+ }
1346+
1347+ #[ tokio:: test]
1348+ async fn test_config_type_mismatch_port_is_string ( ) {
1349+ let toml_str = r#"
1350+ [server]
1351+ port = "not_a_number"
1352+ [upstream]
1353+ "# ;
1354+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1355+ }
1356+
1357+ #[ tokio:: test]
1358+ async fn test_config_type_mismatch_port_is_negative ( ) {
1359+ let toml_str = r#"
1360+ [server]
1361+ port = -1
1362+ [upstream]
1363+ "# ;
1364+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1365+ }
1366+
1367+ #[ tokio:: test]
1368+ async fn test_config_type_mismatch_port_too_large ( ) {
1369+ let toml_str = r#"
1370+ [server]
1371+ port = 70000
1372+ [upstream]
1373+ "# ;
1374+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1375+ }
1376+
1377+ #[ tokio:: test]
1378+ async fn test_config_type_mismatch_host_is_number ( ) {
1379+ let toml_str = r#"
1380+ [server]
1381+ host = 12345
1382+ [upstream]
1383+ "# ;
1384+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1385+ }
1386+
1387+ #[ tokio:: test]
1388+ async fn test_config_type_mismatch_scan_responses_is_string ( ) {
1389+ let toml_str = r#"
1390+ [server]
1391+ [upstream]
1392+ [dlp]
1393+ scan_responses = "yes"
1394+ "# ;
1395+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1396+ }
1397+
1398+ #[ tokio:: test]
1399+ async fn test_config_missing_server_section ( ) {
1400+ let toml_str = r#"
1401+ [upstream]
1402+ "# ;
1403+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1404+ }
1405+
1406+ #[ tokio:: test]
1407+ async fn test_config_missing_upstream_section ( ) {
1408+ let toml_str = r#"
1409+ [server]
1410+ "# ;
1411+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1412+ }
1413+
1414+ #[ tokio:: test]
1415+ async fn test_config_missing_both_sections ( ) {
1416+ assert ! ( Config :: parse( "" ) . is_err( ) ) ;
1417+ }
1418+
1419+ #[ tokio:: test]
1420+ async fn test_config_port_zero ( ) {
1421+ let toml_str = r#"
1422+ [server]
1423+ port = 0
1424+ [upstream]
1425+ "# ;
1426+ let config = Config :: parse ( toml_str) . unwrap ( ) ;
1427+ assert_eq ! ( config. server. port, 0 ) ;
1428+ }
1429+
1430+ #[ tokio:: test]
1431+ async fn test_config_port_max ( ) {
1432+ let toml_str = r#"
1433+ [server]
1434+ port = 65535
1435+ [upstream]
1436+ "# ;
1437+ let config = Config :: parse ( toml_str) . unwrap ( ) ;
1438+ assert_eq ! ( config. server. port, 65535 ) ;
1439+ }
1440+
1441+ #[ tokio:: test]
1442+ async fn test_config_empty_host ( ) {
1443+ let toml_str = r#"
1444+ [server]
1445+ host = ""
1446+ [upstream]
1447+ "# ;
1448+ let config = Config :: parse ( toml_str) . unwrap ( ) ;
1449+ assert_eq ! ( config. server. host, "" ) ;
1450+ assert_eq ! ( config. listen_addr( ) , ":18790" ) ;
1451+ }
1452+
1453+ #[ tokio:: test]
1454+ async fn test_config_empty_base_url ( ) {
1455+ let toml_str = r#"
1456+ [server]
1457+ [upstream]
1458+ base_url = ""
1459+ "# ;
1460+ let config = Config :: parse ( toml_str) . unwrap ( ) ;
1461+ assert_eq ! ( config. upstream. base_url, "" ) ;
1462+ }
1463+
1464+ #[ tokio:: test]
1465+ async fn test_config_empty_virtual_key ( ) {
1466+ let toml_str = r#"
1467+ [server]
1468+ [upstream]
1469+ [[keys]]
1470+ virtual_key = ""
1471+ real_key = "sk-real"
1472+ "# ;
1473+ let config = Config :: parse ( toml_str) . unwrap ( ) ;
1474+ assert_eq ! ( config. keys[ 0 ] . virtual_key, "" ) ;
1475+ }
1476+
1477+ #[ tokio:: test]
1478+ async fn test_config_empty_real_key ( ) {
1479+ let toml_str = r#"
1480+ [server]
1481+ [upstream]
1482+ [[keys]]
1483+ virtual_key = "vk-1"
1484+ real_key = ""
1485+ "# ;
1486+ let config = Config :: parse ( toml_str) . unwrap ( ) ;
1487+ assert_eq ! ( config. keys[ 0 ] . real_key, "" ) ;
1488+ }
1489+
1490+ #[ tokio:: test]
1491+ async fn test_config_from_file_nonexistent ( ) {
1492+ let result = Config :: from_file ( std:: path:: Path :: new ( "/tmp/does_not_exist_clawshell.toml" ) ) ;
1493+ assert ! ( result. is_err( ) ) ;
1494+ }
1495+
1496+ #[ tokio:: test]
1497+ async fn test_config_from_file_not_readable ( ) {
1498+ use std:: fs;
1499+ use std:: os:: unix:: fs:: PermissionsExt ;
1500+ let path = std:: path:: Path :: new ( "/tmp/clawshell_unreadable.toml" ) ;
1501+ fs:: write ( path, "[server]\n [upstream]\n " ) . unwrap ( ) ;
1502+ fs:: set_permissions ( path, fs:: Permissions :: from_mode ( 0o000 ) ) . unwrap ( ) ;
1503+ let result = Config :: from_file ( path) ;
1504+ // Restore permissions for cleanup regardless of test outcome
1505+ let _ = fs:: set_permissions ( path, fs:: Permissions :: from_mode ( 0o644 ) ) ;
1506+ let _ = fs:: remove_file ( path) ;
1507+ assert ! ( result. is_err( ) ) ;
1508+ }
1509+
1510+ #[ tokio:: test]
1511+ async fn test_config_from_file_is_directory ( ) {
1512+ let result = Config :: from_file ( std:: path:: Path :: new ( "/tmp" ) ) ;
1513+ assert ! ( result. is_err( ) ) ;
1514+ }
1515+
1516+ #[ tokio:: test]
1517+ async fn test_config_parse_roundtrip_key_map ( ) {
1518+ let toml_str = r#"
1519+ [server]
1520+ host = "10.0.0.1"
1521+ port = 9090
1522+
1523+ [upstream]
1524+ base_url = "https://custom.api.com"
1525+
1526+ [[keys]]
1527+ virtual_key = "vk-a"
1528+ real_key = "sk-a"
1529+
1530+ [[keys]]
1531+ virtual_key = "vk-b"
1532+ real_key = "sk-b"
1533+ provider = "anthropic"
1534+ "# ;
1535+ let config = Config :: parse ( toml_str) . unwrap ( ) ;
1536+
1537+ // Re-serialize the key_map and verify it matches
1538+ let map = config. key_map ( ) ;
1539+ let mut expected = BTreeMap :: new ( ) ;
1540+ expected. insert (
1541+ "vk-a" . to_string ( ) ,
1542+ ( "sk-a" . to_string ( ) , Provider :: Openai ) ,
1543+ ) ;
1544+ expected. insert (
1545+ "vk-b" . to_string ( ) ,
1546+ ( "sk-b" . to_string ( ) , Provider :: Anthropic ) ,
1547+ ) ;
1548+ assert_eq ! ( map, expected) ;
1549+
1550+ // Re-parse the same TOML and verify deterministic output
1551+ let config2 = Config :: parse ( toml_str) . unwrap ( ) ;
1552+ assert_eq ! ( config. server. host, config2. server. host) ;
1553+ assert_eq ! ( config. server. port, config2. server. port) ;
1554+ assert_eq ! ( config. upstream. base_url, config2. upstream. base_url) ;
1555+ assert_eq ! ( config. keys. len( ) , config2. keys. len( ) ) ;
1556+ assert_eq ! ( config. key_map( ) , config2. key_map( ) ) ;
1557+ assert_eq ! ( config. listen_addr( ) , config2. listen_addr( ) ) ;
1558+ assert_eq ! (
1559+ config. upstream_url( Provider :: Openai ) ,
1560+ config2. upstream_url( Provider :: Openai )
1561+ ) ;
1562+ assert_eq ! (
1563+ config. upstream_url( Provider :: Anthropic ) ,
1564+ config2. upstream_url( Provider :: Anthropic )
1565+ ) ;
1566+ }
1567+
1568+ #[ tokio:: test]
1569+ async fn test_config_parse_roundtrip_dlp ( ) {
1570+ let toml_str = r#"
1571+ [server]
1572+ [upstream]
1573+ [dlp]
1574+ scan_responses = false
1575+ patterns = [
1576+ { name = "ssn", regex = '\\b\\d{3}-\\d{2}-\\d{4}\\b', action = "block" },
1577+ { name = "email", regex = '\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b', action = "redact" },
1578+ ]
1579+ "# ;
1580+ let config1 = Config :: parse ( toml_str) . unwrap ( ) ;
1581+ let config2 = Config :: parse ( toml_str) . unwrap ( ) ;
1582+
1583+ assert_eq ! ( config1. dlp. patterns. len( ) , config2. dlp. patterns. len( ) ) ;
1584+ assert_eq ! ( config1. dlp. scan_responses, config2. dlp. scan_responses) ;
1585+ for ( a, b) in config1. dlp . patterns . iter ( ) . zip ( config2. dlp . patterns . iter ( ) ) {
1586+ assert_eq ! ( a. name, b. name) ;
1587+ assert_eq ! ( a. regex, b. regex) ;
1588+ assert_eq ! ( a. action, b. action) ;
1589+ }
1590+ }
1591+
1592+ #[ tokio:: test]
1593+ async fn test_config_dlp_pattern_missing_name ( ) {
1594+ let toml_str = r#"
1595+ [server]
1596+ [upstream]
1597+ [dlp]
1598+ patterns = [
1599+ { regex = '\\d+' },
1600+ ]
1601+ "# ;
1602+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1603+ }
1604+
1605+ #[ tokio:: test]
1606+ async fn test_config_dlp_pattern_missing_regex ( ) {
1607+ let toml_str = r#"
1608+ [server]
1609+ [upstream]
1610+ [dlp]
1611+ patterns = [
1612+ { name = "test" },
1613+ ]
1614+ "# ;
1615+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1616+ }
1617+
1618+ #[ tokio:: test]
1619+ async fn test_config_dlp_invalid_action ( ) {
1620+ let toml_str = r#"
1621+ [server]
1622+ [upstream]
1623+ [dlp]
1624+ patterns = [
1625+ { name = "test", regex = '\\d+', action = "delete" },
1626+ ]
1627+ "# ;
1628+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1629+ }
1630+
1631+ #[ tokio:: test]
1632+ async fn test_config_unknown_provider ( ) {
1633+ let toml_str = r#"
1634+ [server]
1635+ [upstream]
1636+ [[keys]]
1637+ virtual_key = "vk-1"
1638+ real_key = "sk-1"
1639+ provider = "azure"
1640+ "# ;
1641+ assert ! ( Config :: parse( toml_str) . is_err( ) ) ;
1642+ }
1643+
1644+ #[ tokio:: test]
1645+ async fn test_config_extra_fields_ignored ( ) {
1646+ let toml_str = r#"
1647+ [server]
1648+ host = "0.0.0.0"
1649+ unknown_field = true
1650+ [upstream]
1651+ "# ;
1652+ // toml + serde default: unknown fields cause error (deny_unknown_fields)
1653+ // or are silently ignored. Either way, the parse should handle it consistently.
1654+ let result = Config :: parse ( toml_str) ;
1655+ // serde without deny_unknown_fields ignores extras
1656+ if let Ok ( config) = result {
1657+ assert_eq ! ( config. server. host, "0.0.0.0" ) ;
1658+ }
1659+ // If it errors, that's also valid behavior
1660+ }
1661+
13261662// ========== New Feature Tests ==========
13271663
13281664#[ tokio:: test]
0 commit comments