372 lines
15 KiB
Rust
372 lines
15 KiB
Rust
// file: crates/ksp-config-lib/tests/ownership.rs
|
|
// version: 6
|
|
|
|
//! Workspace ownership audits for KSP application configuration boundaries.
|
|
|
|
fn workspace_root() -> std::path::PathBuf {
|
|
let manifest_directory = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
let root = manifest_directory.parent().and_then(std::path::Path::parent);
|
|
return match root {
|
|
std::option::Option::Some(value) => value.to_path_buf(),
|
|
std::option::Option::None => manifest_directory,
|
|
};
|
|
}
|
|
|
|
fn collect_rust_files(directory: &std::path::Path, files: &mut std::vec::Vec<std::path::PathBuf>) {
|
|
let entries = std::fs::read_dir(directory);
|
|
let entries = match entries {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
for entry in entries {
|
|
let entry = match entry {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => continue,
|
|
};
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
collect_rust_files(path.as_path(), files);
|
|
continue;
|
|
}
|
|
if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("rs") {
|
|
files.push(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn collect_json_files(directory: &std::path::Path, files: &mut std::vec::Vec<std::path::PathBuf>) {
|
|
let entries = std::fs::read_dir(directory);
|
|
let entries = match entries {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
for entry in entries {
|
|
let entry = match entry {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => continue,
|
|
};
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
collect_json_files(path.as_path(), files);
|
|
continue;
|
|
}
|
|
if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("json") {
|
|
files.push(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn non_comment_source(source: &str) -> String {
|
|
let mut filtered = String::new();
|
|
for line in source.lines() {
|
|
let trimmed = line.trim_start();
|
|
if trimmed.starts_with("//") {
|
|
continue;
|
|
}
|
|
filtered.push_str(line);
|
|
filtered.push('\n');
|
|
}
|
|
return filtered;
|
|
}
|
|
|
|
fn collect_environment_names(source: &str, names: &mut std::collections::BTreeSet<String>) {
|
|
let bytes = source.as_bytes();
|
|
let mut index: usize = 0;
|
|
while index < bytes.len() {
|
|
let prefix_length = if bytes[index..].starts_with(b"KSPB_") {
|
|
5
|
|
} else if bytes[index..].starts_with(b"KSP_") {
|
|
4
|
|
} else {
|
|
index += 1;
|
|
continue;
|
|
};
|
|
if !environment_name_start_boundary(bytes, index) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
let mut end = index + prefix_length;
|
|
while end < bytes.len() {
|
|
let byte = bytes[end];
|
|
if byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_' {
|
|
end += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
let candidate = std::str::from_utf8(&bytes[index..end]);
|
|
if let std::result::Result::Ok(candidate) = candidate
|
|
&& candidate.len() > prefix_length
|
|
&& !candidate.ends_with('_')
|
|
&& environment_name_end_boundary(bytes, end)
|
|
&& !is_environment_namespace_label(candidate)
|
|
{
|
|
names.insert(candidate.to_owned());
|
|
}
|
|
index = end;
|
|
}
|
|
}
|
|
|
|
fn is_environment_namespace_label(candidate: &str) -> bool {
|
|
return matches!(candidate, "KSP_PUBLIC" | "KSP_SECRET" | "KSPB_PUBLIC" | "KSPB_SECRET");
|
|
}
|
|
|
|
fn environment_name_end_boundary(bytes: &[u8], end: usize) -> bool {
|
|
if end >= bytes.len() {
|
|
return true;
|
|
}
|
|
let next = bytes[end];
|
|
return !next.is_ascii_alphanumeric() && next != b'_';
|
|
}
|
|
|
|
fn environment_name_start_boundary(bytes: &[u8], index: usize) -> bool {
|
|
if index == 0 {
|
|
return true;
|
|
}
|
|
let previous = bytes[index - 1];
|
|
return !previous.is_ascii_alphanumeric() && previous != b'_';
|
|
}
|
|
|
|
fn dotenv_example_assignments(source: &str) -> std::collections::BTreeMap<String, usize> {
|
|
let mut assignments = std::collections::BTreeMap::<String, usize>::new();
|
|
for (line_index, line) in source.lines().enumerate() {
|
|
let mut candidate = line.trim();
|
|
if let std::option::Option::Some(commented) = candidate.strip_prefix('#') {
|
|
candidate = commented.trim_start();
|
|
}
|
|
let separator = candidate.find('=');
|
|
let separator = match separator {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => continue,
|
|
};
|
|
let name = candidate[..separator].trim();
|
|
if (name.starts_with("KSP_") || name.starts_with("KSPB_")) && !name.ends_with('_') {
|
|
assignments.insert(name.to_owned(), line_index);
|
|
}
|
|
}
|
|
return assignments;
|
|
}
|
|
|
|
fn has_preceding_explanatory_comment(source: &str, assignment_line_index: usize) -> bool {
|
|
let lines: std::vec::Vec<&str> = source.lines().collect();
|
|
if assignment_line_index == 0 || assignment_line_index > lines.len() {
|
|
return false;
|
|
}
|
|
let mut index = assignment_line_index;
|
|
while index > 0 {
|
|
index -= 1;
|
|
let trimmed = lines[index].trim();
|
|
if trimmed.is_empty() {
|
|
continue;
|
|
}
|
|
if !trimmed.starts_with('#') {
|
|
return false;
|
|
}
|
|
let comment = trimmed.trim_start_matches('#').trim_start();
|
|
if comment.starts_with("file:") || comment.starts_with("version:") {
|
|
return false;
|
|
}
|
|
if comment.contains('=') && (comment.starts_with("KSP_") || comment.starts_with("KSPB_")) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#[test]
|
|
fn foundational_dependency_direction_does_not_point_back_to_config() {
|
|
let root = workspace_root();
|
|
for crate_name in ["ksp-core-lib", "ksp-logging-lib"] {
|
|
let manifest_path = root.join("crates").join(crate_name).join("Cargo.toml");
|
|
let manifest = std::fs::read_to_string(manifest_path.as_path());
|
|
assert!(manifest.is_ok(), "unable to read {}", manifest_path.display());
|
|
if let std::result::Result::Ok(manifest) = manifest {
|
|
assert!(!manifest.contains("ksp-config-lib"), "{} must not depend on ksp-config-lib", manifest_path.display());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_crates_do_not_read_ksp_environment_directly() {
|
|
let root = workspace_root();
|
|
let crates_directory = root.join("crates");
|
|
let entries = std::fs::read_dir(crates_directory.as_path());
|
|
assert!(entries.is_ok(), "unable to inspect workspace crates at {}", crates_directory.display());
|
|
let entries = match entries {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
for entry in entries {
|
|
let entry = match entry {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => continue,
|
|
};
|
|
if !entry.path().is_dir() || entry.file_name() == std::ffi::OsStr::new("ksp-config-lib") {
|
|
continue;
|
|
}
|
|
let mut rust_files = std::vec::Vec::new();
|
|
collect_rust_files(entry.path().as_path(), &mut rust_files);
|
|
for rust_file in rust_files {
|
|
let source = std::fs::read_to_string(rust_file.as_path());
|
|
assert!(source.is_ok(), "unable to read {}", rust_file.display());
|
|
let source = match source {
|
|
std::result::Result::Ok(value) => non_comment_source(value.as_str()),
|
|
std::result::Result::Err(_) => continue,
|
|
};
|
|
let forbidden = [
|
|
"std::env::var(\"KSP_",
|
|
"std::env::var(\"KSPB_",
|
|
"std::env::var_os(\"KSP_",
|
|
"std::env::var_os(\"KSPB_",
|
|
"env::var(\"KSP_",
|
|
"env::var(\"KSPB_",
|
|
"env::var_os(\"KSP_",
|
|
"env::var_os(\"KSPB_",
|
|
"std::env::vars()",
|
|
"std::env::vars_os()",
|
|
];
|
|
for token in forbidden {
|
|
assert!(!source.contains(token), "{} bypasses ksp-config-lib for KSP/KSPB environment access via {token}", rust_file.display());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_crates_do_not_hardcode_config_managed_physical_files() {
|
|
let root = workspace_root();
|
|
let crates_directory = root.join("crates");
|
|
let entries = std::fs::read_dir(crates_directory.as_path());
|
|
assert!(entries.is_ok(), "unable to inspect workspace crates at {}", crates_directory.display());
|
|
let entries = match entries {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
for entry in entries {
|
|
let entry = match entry {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => continue,
|
|
};
|
|
if !entry.path().is_dir() || entry.file_name() == std::ffi::OsStr::new("ksp-config-lib") {
|
|
continue;
|
|
}
|
|
let mut rust_files = std::vec::Vec::new();
|
|
collect_rust_files(entry.path().as_path(), &mut rust_files);
|
|
for rust_file in rust_files {
|
|
let source = std::fs::read_to_string(rust_file.as_path());
|
|
assert!(source.is_ok(), "unable to read {}", rust_file.display());
|
|
let source = match source {
|
|
std::result::Result::Ok(value) => non_comment_source(value.as_str()),
|
|
std::result::Result::Err(_) => continue,
|
|
};
|
|
for token in [
|
|
"\".env\"",
|
|
"\"composite.ksp-app-wallet-desk.json\"",
|
|
"\"composite.schema.json\"",
|
|
"\"std.logging.json\"",
|
|
"\"std.logging.schema.json\"",
|
|
"\"std.transport.json\"",
|
|
"\"std.transport.schema.json\"",
|
|
"\"std.wallet.json\"",
|
|
"\"std.wallet.schema.json\"",
|
|
] {
|
|
assert!(!source.contains(token), "{} hardcodes Config-managed physical resource {token}; use ksp-config-lib contracts", rust_file.display());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn environment_name_scanner_ignores_namespace_labels_but_keeps_concrete_names() {
|
|
let source = r#"
|
|
const KSP_PUBLIC_LABEL: &str = "KSP_PUBLIC";
|
|
const KSP_SECRET_LABEL: &str = "KSP_SECRET";
|
|
const KSPB_PUBLIC_LABEL: &str = "KSPB_PUBLIC";
|
|
const KSPB_SECRET_LABEL: &str = "KSPB_SECRET";
|
|
const KSP_PUBLIC_RPC_URL: &str = "KSP_PUBLIC_RPC_URL";
|
|
const KSP_SECRET_API_KEY: &str = "KSP_SECRET_API_KEY";
|
|
const KSPB_PUBLIC_ENDPOINT: &str = "KSPB_PUBLIC_ENDPOINT";
|
|
const KSPB_SECRET_TOKEN: &str = "KSPB_SECRET_TOKEN";
|
|
const FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK: &str = "cfg.composite.ksp-app-wallet-desk";
|
|
const PREFIX_KSP_PUBLIC_RPC_URL_SUFFIXED: &str = "not-an-env";
|
|
"#;
|
|
let mut names = std::collections::BTreeSet::<String>::new();
|
|
collect_environment_names(source, &mut names);
|
|
assert!(!names.contains("KSP_PUBLIC"));
|
|
assert!(!names.contains("KSP_SECRET"));
|
|
assert!(!names.contains("KSPB_PUBLIC"));
|
|
assert!(!names.contains("KSPB_SECRET"));
|
|
assert!(names.contains("KSP_PUBLIC_RPC_URL"));
|
|
assert!(names.contains("KSP_SECRET_API_KEY"));
|
|
assert!(names.contains("KSPB_PUBLIC_ENDPOINT"));
|
|
assert!(names.contains("KSPB_SECRET_TOKEN"));
|
|
assert!(!names.contains("KSP_APP_WALLET_DESK"));
|
|
assert!(!names.contains("KSP_PUBLIC_RPC_URL_SUFFIXED"));
|
|
let concrete_source = r#"const ENV_NAME: &str = "KSP_PUBLIC_RPC_URL_SUFFIXED";"#;
|
|
collect_environment_names(concrete_source, &mut names);
|
|
assert!(names.contains("KSP_PUBLIC_RPC_URL_SUFFIXED"));
|
|
}
|
|
|
|
#[test]
|
|
fn dotenv_example_covers_runtime_environment_names_with_comments() {
|
|
let root = workspace_root();
|
|
let mut runtime_names = std::collections::BTreeSet::<String>::new();
|
|
let config_directory = root.join("config");
|
|
let mut json_files = std::vec::Vec::new();
|
|
collect_json_files(config_directory.as_path(), &mut json_files);
|
|
for json_file in json_files {
|
|
let source = std::fs::read_to_string(json_file.as_path());
|
|
assert!(source.is_ok(), "unable to read {}", json_file.display());
|
|
if let std::result::Result::Ok(source) = source {
|
|
collect_environment_names(source.as_str(), &mut runtime_names);
|
|
}
|
|
}
|
|
let crates_directory = root.join("crates");
|
|
let crate_entries = std::fs::read_dir(crates_directory.as_path());
|
|
assert!(crate_entries.is_ok(), "unable to inspect workspace crates at {}", crates_directory.display());
|
|
let crate_entries = match crate_entries {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
for entry in crate_entries {
|
|
let entry = match entry {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => continue,
|
|
};
|
|
let source_directory = entry.path().join("src");
|
|
if !source_directory.is_dir() {
|
|
continue;
|
|
}
|
|
let mut rust_files = std::vec::Vec::new();
|
|
collect_rust_files(source_directory.as_path(), &mut rust_files);
|
|
for rust_file in rust_files {
|
|
let source = std::fs::read_to_string(rust_file.as_path());
|
|
assert!(source.is_ok(), "unable to read {}", rust_file.display());
|
|
if let std::result::Result::Ok(source) = source {
|
|
let source = non_comment_source(source.as_str());
|
|
collect_environment_names(source.as_str(), &mut runtime_names);
|
|
}
|
|
}
|
|
}
|
|
let example_path = root.join(ksp_config_lib::DEFAULT_DOTENV_EXAMPLE_PATH);
|
|
let example = std::fs::read_to_string(example_path.as_path());
|
|
assert!(example.is_ok(), "unable to read canonical environment inventory {}", example_path.display());
|
|
let example = match example {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let assignments = dotenv_example_assignments(example.as_str());
|
|
assert!(!runtime_names.is_empty(), "runtime environment audit should discover at least one concrete KSP/KSPB variable");
|
|
for variable_name in runtime_names {
|
|
let assignment_line = assignments.get(variable_name.as_str());
|
|
assert!(assignment_line.is_some(), ".env.example is missing runtime variable {variable_name}");
|
|
if let std::option::Option::Some(line_index) = assignment_line {
|
|
assert!(
|
|
has_preceding_explanatory_comment(example.as_str(), *line_index),
|
|
".env.example variable {variable_name} must be preceded by an explanatory comment",
|
|
);
|
|
}
|
|
}
|
|
}
|