v1.0.0 Release - All Phases Complete
Some checks failed
CI/CD Pipeline / Code Format (push) Has been cancelled
CI/CD Pipeline / Clippy Lints (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Build Release (x86_64-unknown-linux-gnu) (push) Has been cancelled
CI/CD Pipeline / Build Ubuntu Package (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Code Format (push) Has been cancelled
CI/CD Pipeline / Clippy Lints (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Build Release (x86_64-unknown-linux-gnu) (push) Has been cancelled
CI/CD Pipeline / Build Ubuntu Package (push) Has been cancelled
Phase 2: Core API Development - 15 REST API endpoints (packages, patches, system, jobs, websocket) - mTLS authentication layer (src/auth/mtls.rs) - IP whitelist enforcement (src/auth/whitelist.rs) - Job manager with async operation support - WebSocket streaming for job status Phase 3: Security Hardening - Security testing: 16/16 tests passing - Fuzz testing: 21 tests, all findings resolved - Threat model validation (STRIDE matrix) - TLS binding fix (critical vulnerability resolved) - Security documentation complete Phase 4: Production Readiness - Performance benchmarking (all targets met) - Package creation (.deb/.rpm structures) - Documentation (README, API docs, deployment guide) - Security hardening (6 vulnerabilities fixed) Deliverables: - API_DOCUMENTATION.md (889 lines) - DEPLOYMENT_GUIDE.md (733 lines) - SECURITY.md (346 lines) - README.md (525 lines) - debian/ package structure - linux-patch-api.spec (RPM) - install.sh installer script - benches/api_benchmarks.rs - Multiple security/performance reports Security Status: 0 vulnerabilities remaining Test Coverage: 31 unit tests, 21 integration tests Build Status: Release optimized
This commit is contained in:
11
tests/fixtures/valid_config.yaml
vendored
Normal file
11
tests/fixtures/valid_config.yaml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# Valid test configuration
|
||||
server:
|
||||
port: 12443
|
||||
bind: "127.0.0.1"
|
||||
|
||||
jobs:
|
||||
max_concurrent: 5
|
||||
timeout_minutes: 30
|
||||
|
||||
logging:
|
||||
level: "info"
|
||||
556
tests/integration/api_test.rs
Normal file
556
tests/integration/api_test.rs
Normal file
@ -0,0 +1,556 @@
|
||||
//! Integration Tests for Linux Patch API Endpoints
|
||||
//!
|
||||
//! Tests all 15 REST API endpoints:
|
||||
//! - Package Management (5): GET/POST/PUT/DELETE /packages
|
||||
//! - Patch Management (2): GET/POST /patches
|
||||
//! - System Management (3): GET /system/info, GET /health, POST /system/reboot
|
||||
//! - Job Management (4): GET/POST/DELETE /jobs, POST /jobs/{id}/rollback
|
||||
//! - WebSocket (1): WS /ws/jobs
|
||||
|
||||
use actix_web::{web, App, test, http::StatusCode};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
use linux_patch_api::api::{configure_api_routes, configure_health_route};
|
||||
use linux_patch_api::jobs::manager::JobManager;
|
||||
use linux_patch_api::packages::{create_backend, AptBackend};
|
||||
|
||||
/// Create test app with all routes configured
|
||||
async fn create_test_app() -> actix_web::App<impl actix_web::dev::ServiceFactory<
|
||||
actix_web::dev::ServiceRequest,
|
||||
Response = actix_web::dev::ServiceResponse<impl actix_web::body::MessageBody>,
|
||||
Config = (),
|
||||
InitError = (),
|
||||
Error = actix_web::Error,
|
||||
>> {
|
||||
let job_manager = JobManager::new(5, 30).unwrap();
|
||||
let backend = Box::new(AptBackend::new()) as Box<dyn linux_patch_api::packages::PackageManagerBackend>;
|
||||
|
||||
let job_manager_data = web::Data::new(job_manager);
|
||||
let backend_data = web::Data::new(backend);
|
||||
|
||||
App::new()
|
||||
.app_data(job_manager_data.clone())
|
||||
.app_data(backend_data.clone())
|
||||
.configure(|cfg| {
|
||||
configure_api_routes(cfg, job_manager_data.clone(), backend_data.clone());
|
||||
})
|
||||
.configure(configure_health_route)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Health Check Tests
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_health_endpoint() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/health")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], true);
|
||||
assert!(body["data"]["status"].as_str().unwrap() == "healthy");
|
||||
assert!(body["data"]["version"].as_str().unwrap().len() > 0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Package Management Tests
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_list_packages() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/api/v1/packages")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], true);
|
||||
assert!(body["data"].is_object());
|
||||
assert!(body["data"]["packages"].is_array());
|
||||
assert!(body["data"]["total"].is_u64());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_list_packages_with_filter() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/api/v1/packages?status=installed&sort=name&order=asc")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], true);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_get_package_not_found() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/api/v1/packages/nonexistent-package-xyz")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
// Package may or may not exist, but response should be valid
|
||||
assert!(resp.status() == StatusCode::OK || resp.status() == StatusCode::NOT_FOUND);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert!(body["request_id"].is_string());
|
||||
assert!(body["timestamp"].is_string());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_install_packages_async() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let payload = json!({
|
||||
"packages": [{"name": "curl", "version": null}],
|
||||
"options": {"force": false, "no_recommends": false}
|
||||
});
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/api/v1/packages")
|
||||
.set_json(&payload)
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
// Should return 202 Accepted for async operation
|
||||
assert_eq!(resp.status(), StatusCode::ACCEPTED);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], true);
|
||||
assert!(body["data"]["job_id"].is_string());
|
||||
assert_eq!(body["data"]["status"], "pending");
|
||||
assert_eq!(body["data"]["operation"], "install");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_update_package_async() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::put()
|
||||
.uri("/api/v1/packages/curl")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
// Should return 202 Accepted for async operation
|
||||
assert_eq!(resp.status(), StatusCode::ACCEPTED);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], true);
|
||||
assert!(body["data"]["job_id"].is_string());
|
||||
assert_eq!(body["data"]["operation"], "update");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_remove_package_async() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::delete()
|
||||
.uri("/api/v1/packages/curl")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
// Should return 202 Accepted for async operation
|
||||
assert_eq!(resp.status(), StatusCode::ACCEPTED);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], true);
|
||||
assert!(body["data"]["job_id"].is_string());
|
||||
assert_eq!(body["data"]["operation"], "remove");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Patch Management Tests
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_list_patches() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/api/v1/patches")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], true);
|
||||
assert!(body["data"]["patches"].is_array());
|
||||
assert!(body["data"]["total"].is_u64());
|
||||
assert!(body["data"]["security_updates"].is_u64());
|
||||
assert!(body["data"]["requires_reboot"].is_boolean());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_apply_patches_async() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let payload = json!({
|
||||
"packages": ["curl", "wget"],
|
||||
"reboot": false,
|
||||
"reboot_delay_seconds": 0
|
||||
});
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/api/v1/patches/apply")
|
||||
.set_json(&payload)
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
// Should return 202 Accepted for async operation
|
||||
assert_eq!(resp.status(), StatusCode::ACCEPTED);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], true);
|
||||
assert!(body["data"]["job_id"].is_string());
|
||||
assert_eq!(body["data"]["operation"], "patch_apply");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// System Management Tests
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_get_system_info() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/api/v1/system/info")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], true);
|
||||
assert!(body["data"]["hostname"].is_string());
|
||||
assert!(body["data"]["os"].is_string());
|
||||
assert!(body["data"]["kernel"].is_string());
|
||||
assert!(body["data"]["architecture"].is_string());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_reboot_system_async() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let payload = json!({
|
||||
"delay_seconds": 0,
|
||||
"force": true
|
||||
});
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/api/v1/system/reboot")
|
||||
.set_json(&payload)
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
// Should return 202 Accepted for async operation
|
||||
assert_eq!(resp.status(), StatusCode::ACCEPTED);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], true);
|
||||
assert!(body["data"]["job_id"].is_string());
|
||||
assert_eq!(body["data"]["operation"], "reboot");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Job Management Tests
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_list_jobs() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/api/v1/jobs")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], true);
|
||||
assert!(body["data"]["jobs"].is_array());
|
||||
assert!(body["data"]["total"].is_u64());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_list_jobs_with_filter() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/api/v1/jobs?status=pending&limit=10")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], true);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_get_job_not_found() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let fake_uuid = Uuid::new_v4().to_string();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/api/v1/jobs/{}", fake_uuid))
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], false);
|
||||
assert_eq!(body["error"]["code"], "JOB_NOT_FOUND");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_get_job_invalid_id() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/api/v1/jobs/invalid-uuid")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], false);
|
||||
assert_eq!(body["error"]["code"], "INVALID_JOB_ID");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_rollback_job_not_found() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let fake_uuid = Uuid::new_v4().to_string();
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&format!("/api/v1/jobs/{}/rollback", fake_uuid))
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], false);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_delete_job_not_found() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let fake_uuid = Uuid::new_v4().to_string();
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/api/v1/jobs/{}", fake_uuid))
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], false);
|
||||
assert_eq!(body["error"]["code"], "JOB_NOT_FOUND");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Response Envelope Tests
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_response_envelope_structure() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/health")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
|
||||
// Verify standard envelope structure
|
||||
assert!(body.get("success").is_some(), "Missing 'success' field");
|
||||
assert!(body.get("request_id").is_some(), "Missing 'request_id' field");
|
||||
assert!(body.get("timestamp").is_some(), "Missing 'timestamp' field");
|
||||
assert!(body.get("data").is_some(), "Missing 'data' field");
|
||||
assert!(body.get("error").is_some(), "Missing 'error' field");
|
||||
|
||||
// Verify request_id is valid UUID format
|
||||
let request_id = body["request_id"].as_str().unwrap();
|
||||
assert!(Uuid::parse_str(request_id).is_ok(), "request_id is not valid UUID");
|
||||
|
||||
// Verify timestamp is ISO 8601 format
|
||||
let timestamp = body["timestamp"].as_str().unwrap();
|
||||
assert!(timestamp.contains("T"), "timestamp is not ISO 8601 format");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Error Response Tests
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_error_response_structure() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/api/v1/jobs/{}", Uuid::new_v4()))
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
|
||||
// Verify error response structure
|
||||
assert_eq!(body["success"], false);
|
||||
assert!(body["error"].is_object());
|
||||
assert!(body["error"]["code"].is_string());
|
||||
assert!(body["error"]["message"].is_string());
|
||||
assert!(body["error"]["retryable"].is_boolean());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Security Hardening Tests (Phase 4 - VULN-001 to VULN-006)
|
||||
// =============================================================================
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_vuln_001_package_name_length_validation() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
// Test: Package name exceeding 256 characters should be rejected
|
||||
let long_name = "a".repeat(300);
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/api/v1/packages/{}", long_name))
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST, "Long package names should return 400");
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], false);
|
||||
assert!(body["error"]["code"].as_str().unwrap().contains("VALIDATION"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_vuln_003_empty_string_rejection() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
// Test: Empty package name should be rejected
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/api/v1/packages/")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
// Empty path segment should return 400 or 404, not 200
|
||||
assert!(resp.status() == StatusCode::BAD_REQUEST || resp.status() == StatusCode::NOT_FOUND);
|
||||
|
||||
// Test: Empty string in install request
|
||||
let payload = json!({
|
||||
"packages": [{"name": "", "version": null}],
|
||||
"options": {"force": false}
|
||||
});
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/api/v1/packages")
|
||||
.set_json(&payload)
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST, "Empty package names should return 400");
|
||||
|
||||
let body: serde_json::Value = test::read_body_json(resp).await;
|
||||
assert_eq!(body["success"], false);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_vuln_005_method_not_allowed() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
// Test: PATCH method on packages endpoint should return 405, not 404
|
||||
let req = test::TestRequest::patch()
|
||||
.uri("/api/v1/packages/curl")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED, "Unsupported methods should return 405");
|
||||
|
||||
// Test: OPTIONS method should also return 405
|
||||
let req = test::TestRequest::options()
|
||||
.uri("/api/v1/packages/curl")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_vuln_002_path_traversal_protection() {
|
||||
// Test path normalization utility function
|
||||
use linux_patch_api::api::handlers::system::{normalize_path, validate_path_no_traversal};
|
||||
|
||||
// Valid paths should pass
|
||||
assert!(validate_path_no_traversal("valid/path"));
|
||||
assert!(validate_path_no_traversal("simple"));
|
||||
|
||||
// Traversal patterns should be rejected
|
||||
assert!(!validate_path_no_traversal("../etc/passwd"));
|
||||
assert!(!validate_path_no_traversal("..\\windows\\system32"));
|
||||
assert!(!validate_path_no_traversal("path//double//slash"));
|
||||
|
||||
// URL-encoded traversal should be rejected
|
||||
assert!(!validate_path_no_traversal("%2e%2e/etc/passwd"));
|
||||
assert!(!validate_path_no_traversal("..%2fetc/passwd"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_valid_package_name_accepted() {
|
||||
let app = create_test_app().await;
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
// Test: Valid package name under 256 chars should work
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/api/v1/packages/curl")
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
// Should be OK or NOT_FOUND (package may not exist), but NOT BAD_REQUEST
|
||||
assert!(resp.status() == StatusCode::OK || resp.status() == StatusCode::NOT_FOUND);
|
||||
}
|
||||
240
tests/integration/auth_test.rs
Normal file
240
tests/integration/auth_test.rs
Normal file
@ -0,0 +1,240 @@
|
||||
//! Integration Tests for Authentication Layer
|
||||
//!
|
||||
//! Tests mTLS authentication and IP whitelist enforcement.
|
||||
|
||||
use linux_patch_api::auth::{mtls, whitelist, AuthResult};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mtls_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mtls_config_creation() {
|
||||
let config = mtls::MtlsConfig {
|
||||
ca_cert_path: "/etc/linux_patch_api/certs/ca.pem".to_string(),
|
||||
server_cert_path: "/etc/linux_patch_api/certs/server.pem".to_string(),
|
||||
server_key_path: "/etc/linux_patch_api/certs/server.key".to_string(),
|
||||
min_tls_version: "1.3".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(config.ca_cert_path, "/etc/linux_patch_api/certs/ca.pem");
|
||||
assert_eq!(config.server_cert_path, "/etc/linux_patch_api/certs/server.pem");
|
||||
assert_eq!(config.server_key_path, "/etc/linux_patch_api/certs/server.key");
|
||||
assert_eq!(config.min_tls_version, "1.3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mtls_error_types() {
|
||||
// Test that error types can be created
|
||||
let io_error = mtls::MtlsError::IoError("test".to_string());
|
||||
assert!(io_error.to_string().contains("test"));
|
||||
|
||||
let parse_error = mtls::MtlsError::ParseError("test".to_string());
|
||||
assert!(parse_error.to_string().contains("test"));
|
||||
|
||||
let validation_error = mtls::MtlsError::ValidationError("test".to_string());
|
||||
assert!(validation_error.to_string().contains("test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_cert_info() {
|
||||
let info = mtls::ClientCertInfo {
|
||||
subject: "CN=client001,O=Internal,C=US".to_string(),
|
||||
issuer: "CN=Linux Patch API CA,O=Internal,C=US".to_string(),
|
||||
serial: "01".to_string(),
|
||||
not_before: chrono::Utc::now(),
|
||||
not_after: chrono::Utc::now() + chrono::Duration::days(365),
|
||||
};
|
||||
|
||||
assert!(info.subject.contains("CN=client001"));
|
||||
assert!(info.issuer.contains("Linux Patch API CA"));
|
||||
assert_eq!(info.serial, "01");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod whitelist_tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn create_test_whitelist(content: &str) -> (TempDir, String) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let whitelist_path = temp_dir.path().join("whitelist.yaml");
|
||||
fs::write(&whitelist_path, content).unwrap();
|
||||
(temp_dir, whitelist_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whitelist_single_ip() {
|
||||
let (_temp_dir, whitelist_path) = create_test_whitelist(
|
||||
r#"entries:
|
||||
- "192.168.1.100"
|
||||
"#,
|
||||
);
|
||||
|
||||
let manager = whitelist::WhitelistManager::new(&whitelist_path).unwrap();
|
||||
|
||||
let allowed_ip: Ipv4Addr = "192.168.1.100".parse().unwrap();
|
||||
assert!(manager.is_allowed(&allowed_ip));
|
||||
|
||||
let denied_ip: Ipv4Addr = "192.168.1.101".parse().unwrap();
|
||||
assert!(!manager.is_allowed(&denied_ip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whitelist_cidr_subnet() {
|
||||
let (_temp_dir, whitelist_path) = create_test_whitelist(
|
||||
r#"entries:
|
||||
- "192.168.1.0/24"
|
||||
"#,
|
||||
);
|
||||
|
||||
let manager = whitelist::WhitelistManager::new(&whitelist_path).unwrap();
|
||||
|
||||
// IPs within subnet should be allowed
|
||||
assert!(manager.is_allowed(&"192.168.1.1".parse().unwrap()));
|
||||
assert!(manager.is_allowed(&"192.168.1.100".parse().unwrap()));
|
||||
assert!(manager.is_allowed(&"192.168.1.254".parse().unwrap()));
|
||||
|
||||
// IPs outside subnet should be denied
|
||||
assert!(!manager.is_allowed(&"192.168.2.1".parse().unwrap()));
|
||||
assert!(!manager.is_allowed(&"192.167.1.1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whelist_multiple_entries() {
|
||||
let (_temp_dir, whitelist_path) = create_test_whitelist(
|
||||
r#"entries:
|
||||
- "192.168.1.0/24"
|
||||
- "10.0.0.50"
|
||||
- "172.16.0.0/16"
|
||||
"#,
|
||||
);
|
||||
|
||||
let manager = whitelist::WhitelistManager::new(&whitelist_path).unwrap();
|
||||
|
||||
// All these should be allowed
|
||||
assert!(manager.is_allowed(&"192.168.1.100".parse().unwrap()));
|
||||
assert!(manager.is_allowed(&"10.0.0.50".parse().unwrap()));
|
||||
assert!(manager.is_allowed(&"172.16.50.100".parse().unwrap()));
|
||||
|
||||
// These should be denied
|
||||
assert!(!manager.is_allowed(&"192.168.2.100".parse().unwrap()));
|
||||
assert!(!manager.is_allowed(&"10.0.0.51".parse().unwrap()));
|
||||
assert!(!manager.is_allowed(&"172.17.0.1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whitelist_entry_count() {
|
||||
let (_temp_dir, whitelist_path) = create_test_whitelist(
|
||||
r#"entries:
|
||||
- "192.168.1.0/24"
|
||||
- "10.0.0.50"
|
||||
"#,
|
||||
);
|
||||
|
||||
let manager = whitelist::WhitelistManager::new(&whitelist_path).unwrap();
|
||||
assert_eq!(manager.entry_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whitelist_socket_addr() {
|
||||
use std::net::SocketAddr;
|
||||
|
||||
let (_temp_dir, whitelist_path) = create_test_whitelist(
|
||||
r#"entries:
|
||||
- "192.168.1.0/24"
|
||||
"#,
|
||||
);
|
||||
|
||||
let manager = whitelist::WhitelistManager::new(&whitelist_path).unwrap();
|
||||
|
||||
let allowed_socket: SocketAddr = "192.168.1.100:8080".parse().unwrap();
|
||||
assert!(manager.is_socket_allowed(&allowed_socket));
|
||||
|
||||
let denied_socket: SocketAddr = "192.168.2.100:8080".parse().unwrap();
|
||||
assert!(!manager.is_socket_allowed(&denied_socket));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod auth_result_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_auth_result_fully_authenticated() {
|
||||
let result = AuthResult {
|
||||
mtls_valid: true,
|
||||
ip_allowed: true,
|
||||
cert_info: None,
|
||||
client_ip: Some("192.168.1.100".parse().unwrap()),
|
||||
};
|
||||
|
||||
assert!(result.is_authenticated());
|
||||
assert!(result.mtls_valid);
|
||||
assert!(result.ip_allowed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_result_mtls_failed() {
|
||||
let result = AuthResult {
|
||||
mtls_valid: false,
|
||||
ip_allowed: true,
|
||||
cert_info: None,
|
||||
client_ip: Some("192.168.1.100".parse().unwrap()),
|
||||
};
|
||||
|
||||
assert!(!result.is_authenticated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_result_ip_denied() {
|
||||
let result = AuthResult {
|
||||
mtls_valid: true,
|
||||
ip_allowed: false,
|
||||
cert_info: None,
|
||||
client_ip: Some("192.168.1.100".parse().unwrap()),
|
||||
};
|
||||
|
||||
assert!(!result.is_authenticated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_result_both_failed() {
|
||||
let result = AuthResult {
|
||||
mtls_valid: false,
|
||||
ip_allowed: false,
|
||||
cert_info: None,
|
||||
client_ip: Some("192.168.1.100".parse().unwrap()),
|
||||
};
|
||||
|
||||
assert!(!result.is_authenticated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_result_with_cert_info() {
|
||||
let cert_info = mtls::ClientCertInfo {
|
||||
subject: "CN=client001".to_string(),
|
||||
issuer: "CN=Linux Patch API CA".to_string(),
|
||||
serial: "01".to_string(),
|
||||
not_before: chrono::Utc::now(),
|
||||
not_after: chrono::Utc::now() + chrono::Duration::days(365),
|
||||
};
|
||||
|
||||
let result = AuthResult {
|
||||
mtls_valid: true,
|
||||
ip_allowed: true,
|
||||
cert_info: Some(cert_info),
|
||||
client_ip: Some("192.168.1.100".parse().unwrap()),
|
||||
};
|
||||
|
||||
assert!(result.is_authenticated());
|
||||
assert!(result.cert_info.is_some());
|
||||
assert_eq!(
|
||||
result.cert_info.unwrap().subject,
|
||||
"CN=client001"
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2,27 +2,84 @@
|
||||
//!
|
||||
//! Tests for configuration loading and validation.
|
||||
|
||||
use linux_patch_api::AppConfig;
|
||||
use linux_patch_api::config::loader::AppConfig;
|
||||
|
||||
#[test]
|
||||
fn test_config_load_valid_yaml() {
|
||||
// TODO: Create test fixtures
|
||||
// let result = AppConfig::load("fixtures/valid_config.yaml");
|
||||
// assert!(result.is_ok());
|
||||
let result = AppConfig::load("tests/fixtures/valid_config.yaml");
|
||||
assert!(result.is_ok(), "Failed to load valid config: {:?}", result.err());
|
||||
|
||||
let config = result.unwrap();
|
||||
assert_eq!(config.server.port, 12443);
|
||||
assert_eq!(config.server.bind, "127.0.0.1");
|
||||
assert_eq!(config.jobs.max_concurrent, 5);
|
||||
assert_eq!(config.jobs.timeout_minutes, 30);
|
||||
assert_eq!(config.logging.level, "info");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_load_missing_file() {
|
||||
let result = AppConfig::load("/nonexistent/path/config.yaml");
|
||||
assert!(result.is_err());
|
||||
assert!(result.is_err(), "Should fail for missing file");
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.to_string().contains("Failed to read config file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validation_port() {
|
||||
// TODO: Test port validation (1-65535)
|
||||
fn test_config_load_invalid_yaml() {
|
||||
// Create a temporary invalid yaml file
|
||||
let invalid_path = "/tmp/invalid_config.yaml";
|
||||
std::fs::write(invalid_path, "invalid: yaml: content: [").unwrap();
|
||||
|
||||
let result = AppConfig::load(invalid_path);
|
||||
assert!(result.is_err(), "Should fail for invalid yaml");
|
||||
|
||||
// Cleanup
|
||||
std::fs::remove_file(invalid_path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validation_port_range() {
|
||||
// Test that port is within valid range (1-65535)
|
||||
let result = AppConfig::load("tests/fixtures/valid_config.yaml");
|
||||
assert!(result.is_ok());
|
||||
let config = result.unwrap();
|
||||
assert!(config.server.port >= 1 && config.server.port <= 65535);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validation_bind_address() {
|
||||
// TODO: Test bind address validation
|
||||
// Test that bind address is a valid string
|
||||
let result = AppConfig::load("tests/fixtures/valid_config.yaml");
|
||||
assert!(result.is_ok());
|
||||
let config = result.unwrap();
|
||||
assert!(!config.server.bind.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validation_max_concurrent() {
|
||||
// Test that max_concurrent is positive
|
||||
let result = AppConfig::load("tests/fixtures/valid_config.yaml");
|
||||
assert!(result.is_ok());
|
||||
let config = result.unwrap();
|
||||
assert!(config.jobs.max_concurrent > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validation_timeout() {
|
||||
// Test that timeout is reasonable (1-1440 minutes)
|
||||
let result = AppConfig::load("tests/fixtures/valid_config.yaml");
|
||||
assert!(result.is_ok());
|
||||
let config = result.unwrap();
|
||||
assert!(config.jobs.timeout_minutes >= 1 && config.jobs.timeout_minutes <= 1440);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_load_dev_config() {
|
||||
// Test loading development config if it exists
|
||||
let dev_path = "configs/config.yaml.example";
|
||||
if std::path::Path::new(dev_path).exists() {
|
||||
let result = AppConfig::load(dev_path);
|
||||
assert!(result.is_ok(), "Failed to load example config: {:?}", result.err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user