Files
telemt/src/api/config_store/tests.rs
T
2026-08-22 16:31:49 +03:00

310 lines
11 KiB
Rust

use super::*;
use crate::config::RateLimitBps;
#[tokio::test]
async fn save_sections_preserves_other_tables_and_comments() {
let dir = std::env::temp_dir().join(format!("cfgtest-{}", rand::random::<u64>()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
std::fs::write(
&path,
"# top comment\n[censorship]\ntls_domain = \"old.example\"\n\n[server]\nport = 443\n",
)
.unwrap();
let mut cfg = ProxyConfig::default();
cfg.censorship.tls_domain = "new.example".to_string();
cfg.server.port = 443;
let rev = save_sections_to_disk(&path, &cfg, &["censorship"])
.await
.unwrap();
let written = std::fs::read_to_string(&path).unwrap();
assert!(written.contains("tls_domain = \"new.example\""));
// Untouched comments and tables remain byte content.
assert!(written.contains("# top comment"));
assert!(written.contains("[server]\nport = 443"));
assert_eq!(rev, compute_revision(&written));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn find_bounds_matches_array_of_tables() {
let src = "[server]\nport = 1\n\n[[upstreams]]\nkind = \"a\"\n\n[[upstreams]]\nkind = \"b\"\n";
let bounds = find_toml_table_bounds(src, "upstreams");
assert!(bounds.is_some(), "should locate [[upstreams]] block start");
let (start, end) = bounds.unwrap();
let slice = &src[start..end];
assert!(slice.starts_with("[[upstreams]]"));
// The bound spans through the last upstream block.
assert!(slice.contains("kind = \"b\""));
}
#[test]
fn find_bounds_matches_header_with_inline_comment() {
let src = "[censorship] # notes\ntls_domain = \"a\"\n\n[server]\nport = 1\n";
let bounds = find_toml_table_bounds(src, "censorship");
assert!(bounds.is_some(), "commented header must still match");
let (start, end) = bounds.unwrap();
let slice = &src[start..end];
assert!(slice.starts_with("[censorship] # notes"));
assert!(slice.contains("tls_domain"));
// The bound terminates at the next header.
assert!(!slice.contains("[server]"));
}
#[tokio::test]
async fn save_general_section_keeps_subtables_dotted_without_duplicates() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
tokio::fs::write(
&path,
"[general]\nprefer_ipv6 = false\n\n[general.modes]\ntls = true\n\n\
[general.links]\npublic_host = \"old.example\"\n\n[server]\nport = 443\n",
)
.await
.unwrap();
let mut cfg = ProxyConfig::default();
cfg.general.prefer_ipv6 = true;
save_sections_to_disk(&path, &cfg, &["general"])
.await
.unwrap();
let written = tokio::fs::read_to_string(&path).await.unwrap();
// No bare top-level [modes] / [links] headers leaked.
for line in written.lines() {
let header = line.trim();
assert_ne!(header, "[modes]", "leaked top-level [modes]:\n{written}");
assert_ne!(header, "[links]", "leaked top-level [links]:\n{written}");
}
// Sub-tables kept their dotted prefix exactly once each.
assert_eq!(
written.matches("[general.modes]").count(),
1,
"[general.modes] must appear exactly once:\n{written}"
);
assert_eq!(
written.matches("[general.links]").count(),
1,
"[general.links] must appear exactly once:\n{written}"
);
// Result parses (duplicate tables would error here).
toml::from_str::<toml::Value>(&written)
.unwrap_or_else(|e| panic!("written config must parse: {e}\n{written}"));
// The unrelated table remains untouched.
assert!(written.contains("[server]\nport = 443"));
}
#[tokio::test]
async fn save_general_section_is_idempotent_across_repeated_saves() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
tokio::fs::write(
&path,
"[general]\nprefer_ipv6 = false\n\n[general.modes]\ntls = true\n\n\
[general.links]\npublic_host = \"old.example\"\n",
)
.await
.unwrap();
let mut cfg = ProxyConfig::default();
cfg.general.prefer_ipv6 = true;
save_sections_to_disk(&path, &cfg, &["general"])
.await
.unwrap();
save_sections_to_disk(&path, &cfg, &["general"])
.await
.unwrap();
let written = tokio::fs::read_to_string(&path).await.unwrap();
assert_eq!(written.matches("[general.modes]").count(), 1, "{written}");
assert_eq!(written.matches("[general.links]").count(), 1, "{written}");
assert_eq!(written.matches("[general]").count(), 1, "{written}");
toml::from_str::<toml::Value>(&written)
.unwrap_or_else(|e| panic!("written config must parse: {e}\n{written}"));
}
#[test]
fn find_bounds_spans_dotted_subtables() {
let src = "[general]\nprefer_ipv6 = false\n\n[general.modes]\ntls = true\n\n\
[general.links]\npublic_host = \"a\"\n\n[server]\nport = 1\n";
let bounds = find_toml_table_bounds(src, "general");
assert!(bounds.is_some(), "should locate [general] block");
let (start, end) = bounds.unwrap();
let slice = &src[start..end];
assert!(slice.starts_with("[general]"));
// Nested sub-tables belong to the parent table bound.
assert!(slice.contains("[general.modes]"));
assert!(slice.contains("[general.links]"));
// The bound terminates before an unrelated header.
assert!(!slice.contains("[server]"));
}
#[test]
fn find_bounds_does_not_overrun_sibling_prefix() {
// access.users must not swallow access.user_enabled (dot guards the prefix).
let src = "[access.users]\nalice = \"x\"\n\n[access.user_enabled]\nalice = true\n";
let bounds = find_toml_table_bounds(src, "access.users").unwrap();
let slice = &src[bounds.0..bounds.1];
assert!(slice.starts_with("[access.users]"));
assert!(!slice.contains("[access.user_enabled]"));
}
#[test]
fn nested_include_detection_does_not_reject_similar_access_keys() {
assert!(has_include_inside_table(
"[access.users]\ninclude = \"users.toml\"\n"
));
assert!(!has_include_inside_table(
"[access.users]\ninclude_user = \"00000000000000000000000000000000\"\n"
));
}
#[tokio::test]
async fn save_general_handles_non_contiguous_subtables() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
// Hand-edited layout: [general.modes] sits AFTER an unrelated [server].
tokio::fs::write(
&path,
"[general]\nprefer_ipv6 = false\n\n[server]\nport = 443\n\n\
[general.modes]\ntls = true\n",
)
.await
.unwrap();
let mut cfg = ProxyConfig::default();
cfg.general.prefer_ipv6 = true;
save_sections_to_disk(&path, &cfg, &["general"])
.await
.unwrap();
let written = tokio::fs::read_to_string(&path).await.unwrap();
assert_eq!(
written.matches("[general.modes]").count(),
1,
"non-contiguous [general.modes] must not duplicate:\n{written}"
);
toml::from_str::<toml::Value>(&written)
.unwrap_or_else(|e| panic!("written config must parse: {e}\n{written}"));
// The unrelated section remains present.
assert!(written.contains("[server]"));
}
#[tokio::test]
async fn manifest_revision_changes_when_an_included_source_changes() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("config.toml");
let included = dir.path().join("included.toml");
tokio::fs::write(&root, "include = \"included.toml\"\n")
.await
.unwrap();
tokio::fs::write(&included, "[censorship]\ntls_domain = \"one.example\"\n")
.await
.unwrap();
let first = current_revision(&root).await.unwrap();
tokio::fs::write(&included, "[censorship]\ntls_domain = \"two.example\"\n")
.await
.unwrap();
let second = current_revision(&root).await.unwrap();
assert_ne!(first, second);
}
#[tokio::test]
async fn manifest_revision_does_not_require_typed_config_validation() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("config.toml");
tokio::fs::write(&root, "[server]\nport = \"invalid\"\n")
.await
.unwrap();
let revision = current_revision(&root).await.unwrap();
assert_eq!(revision.len(), 64);
assert!(load_config_snapshot(&root, false).await.is_err());
}
#[tokio::test]
async fn access_mutation_writes_only_the_single_included_owner() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("config.toml");
let included = dir.path().join("users.toml");
let root_body = "include = \"users.toml\"\n[censorship]\ntls_domain = \"example.com\"\n";
let included_body = "[access.users]\nalice = \"00000000000000000000000000000000\"\n";
tokio::fs::write(&root, root_body).await.unwrap();
tokio::fs::write(&included, included_body).await.unwrap();
let mut cfg = load_config_from_disk(&root).await.unwrap();
cfg.access.users.insert(
"bob".to_string(),
"11111111111111111111111111111111".to_string(),
);
let revision = save_access_sections_to_disk(&root, &cfg, &[AccessSection::Users])
.await
.unwrap();
assert_eq!(tokio::fs::read_to_string(&root).await.unwrap(), root_body);
let written = tokio::fs::read_to_string(&included).await.unwrap();
assert!(written.contains("bob = \"11111111111111111111111111111111\""));
assert_eq!(revision, current_revision(&root).await.unwrap());
}
#[tokio::test]
async fn access_mutation_rejects_sections_with_different_source_owners() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("config.toml");
let included = dir.path().join("enabled.toml");
let root_body = concat!(
"include = \"enabled.toml\"\n",
"[access.users]\nalice = \"00000000000000000000000000000000\"\n"
);
let included_body = "[access.user_enabled]\nalice = false\n";
tokio::fs::write(&root, root_body).await.unwrap();
tokio::fs::write(&included, included_body).await.unwrap();
let cfg = load_config_from_disk(&root).await.unwrap();
let error = save_access_sections_to_disk(
&root,
&cfg,
&[AccessSection::Users, AccessSection::UserEnabled],
)
.await
.unwrap_err();
assert_eq!(error.code, "config_patch_not_atomic");
assert_eq!(tokio::fs::read_to_string(&root).await.unwrap(), root_body);
assert_eq!(
tokio::fs::read_to_string(&included).await.unwrap(),
included_body
);
}
#[test]
fn render_user_rate_limits_section() {
let mut cfg = ProxyConfig::default();
cfg.access.user_rate_limits.insert(
"alice".to_string(),
RateLimitBps {
up_bps: 1024,
down_bps: 2048,
},
);
let rendered =
render_access_section(&cfg, AccessSection::UserRateLimits).expect("section must render");
assert!(rendered.starts_with("[access.user_rate_limits]\n"));
assert!(rendered.contains("alice = { up_bps = 1024, down_bps = 2048 }"));
}