Skip to content

Commit 441346d

Browse files
committed
tests(config): add more tests for config parsing
1 parent 10ae8bd commit 441346d

1 file changed

Lines changed: 336 additions & 0 deletions

File tree

‎tests/integration.rs‎

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)