Compare commits
No commits in common. "f411df0272a20e7944a089748383d1654e406873" and "2a9e4f59faf77835ef1737c1244bc6c238d35931" have entirely different histories.
f411df0272
...
2a9e4f59fa
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,7 +10,6 @@ CLAUDE.md
|
|||||||
.cache
|
.cache
|
||||||
.coder
|
.coder
|
||||||
.wrks
|
.wrks
|
||||||
rollback_instruction*
|
|
||||||
ROOT
|
ROOT
|
||||||
OLD
|
OLD
|
||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
|
|||||||
@ -41,30 +41,6 @@ repos:
|
|||||||
# pass_filenames: false
|
# pass_filenames: false
|
||||||
# stages: [pre-push]
|
# stages: [pre-push]
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Nushell Hooks (ACTIVE)
|
|
||||||
# ============================================================================
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: nushell-check
|
|
||||||
name: Nushell IDE Check (nu --ide-check)
|
|
||||||
entry: bash -c 'for f in $(git diff --cached --name-only | grep "\.nu$"); do nu --ide-check "$f" || exit 1; done'
|
|
||||||
language: system
|
|
||||||
files: \.nu$
|
|
||||||
pass_filenames: false
|
|
||||||
stages: [pre-commit]
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Bash Hooks (ACTIVE)
|
|
||||||
# ============================================================================
|
|
||||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
|
||||||
rev: v0.9.1.1
|
|
||||||
hooks:
|
|
||||||
- id: shellcheck
|
|
||||||
name: Bash linting (shellcheck)
|
|
||||||
args: ['--severity=warning']
|
|
||||||
stages: [pre-commit]
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Nickel Hooks (ACTIVE)
|
# Nickel Hooks (ACTIVE)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@ -505,20 +505,10 @@ impl ToolRegistry {
|
|||||||
|
|
||||||
let mut settings_tools = self.settings_tools.lock().await;
|
let mut settings_tools = self.settings_tools.lock().await;
|
||||||
|
|
||||||
let settings = settings_tools
|
settings_tools
|
||||||
.get_settings(query)
|
.get_settings(query)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to get settings: {}", e))?;
|
.map_err(|e| format!("Failed to get settings: {}", e))
|
||||||
|
|
||||||
Ok(json!({
|
|
||||||
"status": "success",
|
|
||||||
"tool": "installer_get_settings",
|
|
||||||
"platforms": settings.get("platforms"),
|
|
||||||
"modes": settings.get("modes"),
|
|
||||||
"default_domain": settings.get("default_domain"),
|
|
||||||
"auto_generate_secrets": settings.get("auto_generate_secrets"),
|
|
||||||
"available_services": settings.get("available_services")
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn installer_complete_config(&self, args: &Value) -> Result<Value, String> {
|
async fn installer_complete_config(&self, args: &Value) -> Result<Value, String> {
|
||||||
@ -556,22 +546,9 @@ impl ToolRegistry {
|
|||||||
|
|
||||||
let settings_tools = self.settings_tools.lock().await;
|
let settings_tools = self.settings_tools.lock().await;
|
||||||
|
|
||||||
let defaults = settings_tools
|
settings_tools
|
||||||
.get_mode_defaults(mode)
|
.get_mode_defaults(mode)
|
||||||
.map_err(|e| format!("Failed to get defaults: {}", e))?;
|
.map_err(|e| format!("Failed to get defaults: {}", e))
|
||||||
|
|
||||||
Ok(json!({
|
|
||||||
"status": "success",
|
|
||||||
"tool": "installer_get_defaults",
|
|
||||||
"mode": defaults.get("mode"),
|
|
||||||
"description": defaults.get("description"),
|
|
||||||
"service_count": defaults.get("service_count"),
|
|
||||||
"min_cpu_cores": defaults.get("min_cpu_cores"),
|
|
||||||
"min_memory_gb": defaults.get("min_memory_gb"),
|
|
||||||
"recommended_services": defaults.get("recommended_services"),
|
|
||||||
"auto_generate_secrets": defaults.get("auto_generate_secrets"),
|
|
||||||
"domain": defaults.get("domain")
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn installer_platform_recommendations(&self, _args: &Value) -> Result<Value, String> {
|
async fn installer_platform_recommendations(&self, _args: &Value) -> Result<Value, String> {
|
||||||
|
|||||||
@ -100,13 +100,12 @@ async fn test_explicit_tool_call_rag_ask() {
|
|||||||
args: json!({"question": "What is Nushell?"}),
|
args: json!({"question": "What is Nushell?"}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
let response = service.call_mcp_tool(req).await.unwrap();
|
||||||
assert_eq!(response.result["status"], "success");
|
assert_eq!(response.result["status"], "success");
|
||||||
assert_eq!(response.result["tool"], "rag_ask_question");
|
assert_eq!(response.result["tool"], "rag_ask_question");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore]
|
|
||||||
async fn test_explicit_tool_call_guidance_status() {
|
async fn test_explicit_tool_call_guidance_status() {
|
||||||
let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap();
|
let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap();
|
||||||
let service = AiService::new(addr);
|
let service = AiService::new(addr);
|
||||||
@ -116,7 +115,7 @@ async fn test_explicit_tool_call_guidance_status() {
|
|||||||
args: json!({}),
|
args: json!({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = service.call_mcp_tool(req).await.expect("guidance_check_system_status failed");
|
let response = service.call_mcp_tool(req).await.unwrap();
|
||||||
assert_eq!(response.result["status"], "healthy");
|
assert_eq!(response.result["status"], "healthy");
|
||||||
assert_eq!(response.result["tool"], "guidance_check_system_status");
|
assert_eq!(response.result["tool"], "guidance_check_system_status");
|
||||||
}
|
}
|
||||||
@ -131,7 +130,7 @@ async fn test_explicit_tool_call_settings() {
|
|||||||
args: json!({}),
|
args: json!({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
let response = service.call_mcp_tool(req).await.unwrap();
|
||||||
assert_eq!(response.result["status"], "success");
|
assert_eq!(response.result["status"], "success");
|
||||||
// Verify real SettingsTools data is returned (not empty placeholder)
|
// Verify real SettingsTools data is returned (not empty placeholder)
|
||||||
assert!(
|
assert!(
|
||||||
@ -152,7 +151,7 @@ async fn test_settings_tools_platform_recommendations() {
|
|||||||
args: json!({}),
|
args: json!({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
let response = service.call_mcp_tool(req).await.unwrap();
|
||||||
assert_eq!(response.result["status"], "success");
|
assert_eq!(response.result["status"], "success");
|
||||||
// Should have real recommendations array from SettingsTools platform detection
|
// Should have real recommendations array from SettingsTools platform detection
|
||||||
assert!(response.result.get("recommendations").is_some());
|
assert!(response.result.get("recommendations").is_some());
|
||||||
@ -168,7 +167,7 @@ async fn test_settings_tools_mode_defaults() {
|
|||||||
args: json!({"mode": "solo"}),
|
args: json!({"mode": "solo"}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
let response = service.call_mcp_tool(req).await.unwrap();
|
||||||
assert_eq!(response.result["status"], "success");
|
assert_eq!(response.result["status"], "success");
|
||||||
// Verify real mode defaults (resource requirements)
|
// Verify real mode defaults (resource requirements)
|
||||||
assert!(response.result.get("min_cpu_cores").is_some());
|
assert!(response.result.get("min_cpu_cores").is_some());
|
||||||
@ -185,7 +184,7 @@ async fn test_explicit_tool_call_iac() {
|
|||||||
args: json!({"path": "/tmp/infra"}),
|
args: json!({"path": "/tmp/infra"}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
let response = service.call_mcp_tool(req).await.unwrap();
|
||||||
assert_eq!(response.result["status"], "success");
|
assert_eq!(response.result["status"], "success");
|
||||||
// Verify real technology detection (returns technologies array)
|
// Verify real technology detection (returns technologies array)
|
||||||
assert!(response.result.get("technologies").is_some());
|
assert!(response.result.get("technologies").is_some());
|
||||||
@ -202,7 +201,7 @@ async fn test_iac_detect_technologies_real() {
|
|||||||
args: json!({"path": "../../provisioning"}),
|
args: json!({"path": "../../provisioning"}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
let response = service.call_mcp_tool(req).await.unwrap();
|
||||||
assert_eq!(response.result["status"], "success");
|
assert_eq!(response.result["status"], "success");
|
||||||
|
|
||||||
// Should detect technologies as an array
|
// Should detect technologies as an array
|
||||||
@ -221,7 +220,7 @@ async fn test_iac_analyze_completeness() {
|
|||||||
args: json!({"path": "/tmp/test-infra"}),
|
args: json!({"path": "/tmp/test-infra"}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
let response = service.call_mcp_tool(req).await.unwrap();
|
||||||
assert_eq!(response.result["status"], "success");
|
assert_eq!(response.result["status"], "success");
|
||||||
// Verify real analysis data
|
// Verify real analysis data
|
||||||
assert!(response.result.get("complete").is_some());
|
assert!(response.result.get("complete").is_some());
|
||||||
@ -365,7 +364,7 @@ async fn test_tool_execution_with_required_args() {
|
|||||||
args: json!({"query": "kubernetes"}),
|
args: json!({"query": "kubernetes"}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
let response = service.call_mcp_tool(req).await.unwrap();
|
||||||
assert_eq!(response.result["status"], "success");
|
assert_eq!(response.result["status"], "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -86,8 +86,7 @@ impl TokenClaims {
|
|||||||
pub fn needs_rotation(&self) -> bool {
|
pub fn needs_rotation(&self) -> bool {
|
||||||
let now = Utc::now().timestamp();
|
let now = Utc::now().timestamp();
|
||||||
let time_until_expiry = self.exp - now;
|
let time_until_expiry = self.exp - now;
|
||||||
// Needs rotation if within 5 minutes AND not yet expired
|
time_until_expiry <= 300 // 5 minutes
|
||||||
time_until_expiry <= 300 && time_until_expiry > 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get remaining validity duration
|
/// Get remaining validity duration
|
||||||
@ -172,7 +171,7 @@ impl JwtService {
|
|||||||
/// &private_key,
|
/// &private_key,
|
||||||
/// &public_key,
|
/// &public_key,
|
||||||
/// "control-center",
|
/// "control-center",
|
||||||
/// vec!["orchestrator".to_string(), "cli".to_string()]
|
/// vec!["orchestrator", "cli"]
|
||||||
/// ).unwrap();
|
/// ).unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new(
|
pub fn new(
|
||||||
@ -470,16 +469,48 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn generate_test_keys() -> (Vec<u8>, Vec<u8>) {
|
fn generate_test_keys() -> (Vec<u8>, Vec<u8>) {
|
||||||
// Generate fresh RSA keys for testing
|
// Pre-generated RSA keys to avoid runtime key generation (avoids rand_core
|
||||||
use crate::services::jwt::generate_rsa_key_pair;
|
// conflict)
|
||||||
|
let private_pem = b"-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7F43HxrVfJJ+k
|
||||||
|
DQMEjENGqJLnBn6MvJnCu93A4ZNKEEpPGX1Y6V+qiqLH5B7wNMIJ2QVnLjYjKZu5
|
||||||
|
K5LkVCH8N7N1s9QXiLI7TzQVEH2TQx3MzQFRmVzGlyVFxF0qMHf/mNlZvE0Hb9oI
|
||||||
|
YdYnBW5k4TRmzrN4THEbWqLcGVVfMVFQqG1j/V3G5dTpV1sN8a9xqJG0dKNmVP2M
|
||||||
|
H5JG3kQ8CxFqp7pRsGvqH9IVjB6KXj8L3VPQYJxIBKHGZMlsP9SVqH3vwqLLKcBj
|
||||||
|
G2UvhkVYJ6ZLvYIj7LvZKrCBrP3h5Q7hYpXZqDGYd5dDwWFJ3xkVMkJMGR3D3VrK
|
||||||
|
R1jvJTEhAgMBAAECggEAXV2TZjfRbhR0IaqU3bpSZvb+6uL+5OgWRO5VbG0XGFmv
|
||||||
|
1v+2F5hQEZYKKxZvTlF4IxN1J0YQRKt4BG6PZqN9+Uj8z7DVEDqiWLIZSEhUPzVu
|
||||||
|
E3IKZaEqvGKvEwBglmfqnE6cEZ2Kb9aOx1a5N8UH5q7xQ2d4YxKfYwRBkDYEZmZp
|
||||||
|
R4tEb0qfKFgIGmCfK5fNvJqXvH/f1xq9QW1qrGYqGAqh7bkxvXGfkRBXh7Q8sAXW
|
||||||
|
B2XQ8L9CZV5lsP0F7n8nKvDVKfqZfGXlZJZQvJPF5L8p6g7V7uE+6bZlF4YZLRvW
|
||||||
|
2aq2xZR6bGCWxBpLb2lG1QU0aJLqVmVjAQYDM9PB0QKBgQDfhKn8F5aITVFJhVVu
|
||||||
|
BN8F2hLRtFDfSPTN0JM1WZq/EqYdLjnlAY8L7kBBzNaKgOGBRqfO1lVv7DG4vPtX
|
||||||
|
nrLaM3SU3F0lC8tVwqJTUvZS5/Pl3wqAZRlWbWm4Yp6gWXlGPTaWRpGRBOqB+rWP
|
||||||
|
b8rX2SnZjvFlQZlZPfvHGWpkFQKBgQDYtvXAGNdj8hMZ6mMUKYv3YbF7F2xUlf1x
|
||||||
|
H0f8I5QGPiZoqzNGJXCEGvNVNEUgR7LY6AeqWtLqRKJsQ5NYb2XzLhA2BVOV1BV2
|
||||||
|
Z1Y3s0V5XjFE4q7gWmEcLlWyEiKiRCRQC4T7qB+gUQ8lFBrGqQ4vZqE+7pQyYfCt
|
||||||
|
1yFQnvqrQQKBgQCcvCvE4wMhF2lYFxM4v8qCDXJ9nLMKjYPFn3C0YrP6Zn2Pm7r3
|
||||||
|
fJGDVBmN5wVLDXqZnSrmh0iBs7PdBqeHALF7jTj9X8tNe+hpXwQ/GWSF8b3q1b6k
|
||||||
|
wZLhKpJQGpYH0M3l7B6rrM3Vr1E/DQjXvXdqXrL3vKfHQrEk3c9zVCkfTQKBgGVl
|
||||||
|
qdvBYvdgWHvPzL3+vqvLPVKDnLjN6hXA2cJGaBNpOlm+bI5cJLLnWhJrjCkK+POo
|
||||||
|
0oPKvnO5qKQjRDWLVCJH3z8MxPjqV6Yk1hAg/4vqV3oQsJDfU3qiDPAHrBbKBlRm
|
||||||
|
4KYmEqIhqNYJqXN7nnQFOLYFxOaXKqe6HBh1g4QBAoGBAIRXPwKJhwIUfLJ8DQAO
|
||||||
|
9z9YH3eIiGkLxF8f7WZvVnNwMEpXl4IqN8N3qGXKJfXJI1YfBKXN7LOqCOVFGkVq
|
||||||
|
JkVN8qPTwAKU8E2U3dGzQVPP2JmGPPNyZJxQJ3sYtKnLkz2YdEbKDhkG5wPDHKKu
|
||||||
|
LfEK1YMqL/VvAJdIXxqZqDXf
|
||||||
|
-----END PRIVATE KEY-----";
|
||||||
|
|
||||||
let keys = generate_rsa_key_pair()
|
let public_pem = b"-----BEGIN PUBLIC KEY-----
|
||||||
.expect("Failed to generate test RSA keys");
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuxeNx8a1XySfpA0DBIxD
|
||||||
|
RqiS5wZ+jLyZwrvdwOGTShBKTxl9WOlfqoqix+Qe8DTCCdkFZy42IymbhSuS5FQh
|
||||||
|
/DezdLPUF4iyO080FRB9k0MdzM0BUZlcxpclRcRdKjB3/5jZWbxNB2/aCGHWJwVu
|
||||||
|
ZOE0Zs6zeExxG1qi3BlVXzFRUKhtY/1dxuXU6Vdbl/GvcaiRtHSjZlT9jB+SRt5E
|
||||||
|
PAsTaqe6UbBr6h/SFYweil4/C91T0GCcSAShxmTJbD/Ulah978KizynAYxtlL4ZF
|
||||||
|
WCemS72CI+y72SqwgaZ94eUO4WKV2agxmHeXQ8FhSd8ZFTJCTBKDW91Kykdo7yUx
|
||||||
|
IQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----";
|
||||||
|
|
||||||
(
|
(private_pem.to_vec(), public_pem.to_vec())
|
||||||
keys.private_key_pem.into_bytes(),
|
|
||||||
keys.public_key_pem.into_bytes(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_test_service() -> JwtService {
|
fn create_test_service() -> JwtService {
|
||||||
|
|||||||
@ -65,7 +65,7 @@
|
|||||||
//! # Usage Example
|
//! # Usage Example
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! # use control_center::auth::{JwtService, PasswordService};
|
//! # use control_center::auth::{JwtService, PasswordService, User};
|
||||||
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
//! // Initialize services
|
//! // Initialize services
|
||||||
//! let private_key = std::fs::read("keys/private.pem")?;
|
//! let private_key = std::fs::read("keys/private.pem")?;
|
||||||
@ -80,12 +80,19 @@
|
|||||||
//!
|
//!
|
||||||
//! let password_service = PasswordService::new();
|
//! let password_service = PasswordService::new();
|
||||||
//!
|
//!
|
||||||
//! // Verify password
|
//! // Create user with hashed password
|
||||||
//! let password_hash = password_service.hash_password("secure_password")?;
|
//! let password_hash = password_service.hash_password("secure_password")?;
|
||||||
//! if password_service.verify_password("secure_password", &password_hash)? {
|
//! let user = User {
|
||||||
//! // Generate token pair for authenticated user
|
//! id: "user123".to_string(),
|
||||||
|
//! username: "alice".to_string(),
|
||||||
|
//! password_hash,
|
||||||
|
//! // ... other fields
|
||||||
|
//! };
|
||||||
|
//!
|
||||||
|
//! // Login: verify password and generate tokens
|
||||||
|
//! if password_service.verify_password("secure_password", &user.password_hash)? {
|
||||||
//! let tokens = jwt_service.generate_token_pair(
|
//! let tokens = jwt_service.generate_token_pair(
|
||||||
//! "user123",
|
//! &user.id,
|
||||||
//! "workspace1",
|
//! "workspace1",
|
||||||
//! "permissions_hash",
|
//! "permissions_hash",
|
||||||
//! None,
|
//! None,
|
||||||
@ -93,20 +100,20 @@
|
|||||||
//!
|
//!
|
||||||
//! println!("Access token: {}", tokens.access_token);
|
//! println!("Access token: {}", tokens.access_token);
|
||||||
//! println!("Expires in: {} seconds", tokens.expires_in);
|
//! println!("Expires in: {} seconds", tokens.expires_in);
|
||||||
//!
|
|
||||||
//! // Validate token
|
|
||||||
//! let claims = jwt_service.validate_token(&tokens.access_token)?;
|
|
||||||
//! println!("User: {}, Workspace: {}", claims.sub, claims.workspace);
|
|
||||||
//!
|
|
||||||
//! // Rotate token before expiry
|
|
||||||
//! if claims.needs_rotation() {
|
|
||||||
//! let new_tokens = jwt_service.rotate_token(&tokens.refresh_token)?;
|
|
||||||
//! println!("New access token: {}", new_tokens.access_token);
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! // Revoke token (logout)
|
|
||||||
//! jwt_service.revoke_token(&claims.jti, claims.exp)?;
|
|
||||||
//! }
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Validate token
|
||||||
|
//! let claims = jwt_service.validate_token(&tokens.access_token)?;
|
||||||
|
//! println!("User: {}, Workspace: {}", claims.sub, claims.workspace);
|
||||||
|
//!
|
||||||
|
//! // Rotate token before expiry
|
||||||
|
//! if claims.needs_rotation() {
|
||||||
|
//! let new_tokens = jwt_service.rotate_token(&tokens.refresh_token)?;
|
||||||
|
//! println!("New access token: {}", new_tokens.access_token);
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Revoke token (logout)
|
||||||
|
//! jwt_service.revoke_token(&claims.jti, claims.exp)?;
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
|
|||||||
@ -157,9 +157,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_password_strength_fair() {
|
fn test_password_strength_fair() {
|
||||||
let service = PasswordService::new();
|
let service = PasswordService::new();
|
||||||
// Fair: 8-9 chars with 0-2 complexity types (lowercase, uppercase, digit, special)
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
service.evaluate_strength("password1"), // 9 chars, 2 types: lowercase + digit
|
service.evaluate_strength("Password1"),
|
||||||
PasswordStrength::Fair
|
PasswordStrength::Fair
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,12 +23,12 @@ use crate::kms::types::{KeyData, KeyType, ProviderCredentials};
|
|||||||
/// # Usage
|
/// # Usage
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// use control_center::kms::{KmsService, KeyType};
|
/// use control_center::kms::{KmsService, KmsConfig};
|
||||||
/// use std::sync::Arc;
|
/// use std::sync::Arc;
|
||||||
///
|
///
|
||||||
/// async fn example(kms: Arc<dyn KmsService>) -> anyhow::Result<()> {
|
/// async fn example(kms: Arc<dyn KmsService>) -> anyhow::Result<()> {
|
||||||
/// // Use the trait, not the concrete type
|
/// // Use the trait, not the concrete type
|
||||||
/// let key = kms.generate_key(KeyType::Asymmetric).await?;
|
/// let key = kms.generate_key(/* params */).await?;
|
||||||
/// println!("Generated key: {}", key.key_id);
|
/// println!("Generated key: {}", key.key_id);
|
||||||
/// Ok(())
|
/// Ok(())
|
||||||
/// }
|
/// }
|
||||||
|
|||||||
@ -356,25 +356,9 @@ mod tests {
|
|||||||
assert!(admin_context.has_permission("system:manage"));
|
assert!(admin_context.has_permission("system:manage"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_test_jwt_config() -> JwtConfig {
|
|
||||||
use crate::services::jwt::generate_rsa_key_pair;
|
|
||||||
let keys = generate_rsa_key_pair().expect("Failed to generate test keys");
|
|
||||||
JwtConfig {
|
|
||||||
issuer: "test-issuer".to_string(),
|
|
||||||
audience: "test-audience".to_string(),
|
|
||||||
access_token_expiration_hours: 1,
|
|
||||||
refresh_token_expiration_hours: 24,
|
|
||||||
private_key_pem: keys.private_key_pem,
|
|
||||||
public_key_pem: keys.public_key_pem,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_auth_header_parsing() {
|
async fn test_auth_header_parsing() {
|
||||||
let jwt_service = Arc::new(
|
let jwt_service = Arc::new(JwtService::new(JwtConfig::default()).unwrap());
|
||||||
JwtService::new(create_test_jwt_config())
|
|
||||||
.expect("Failed to create JWT service for test")
|
|
||||||
);
|
|
||||||
|
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
let session_id = Uuid::new_v4();
|
let session_id = Uuid::new_v4();
|
||||||
@ -382,13 +366,13 @@ mod tests {
|
|||||||
|
|
||||||
let token = jwt_service
|
let token = jwt_service
|
||||||
.generate_access_token(user_id, session_id, roles)
|
.generate_access_token(user_id, session_id, roles)
|
||||||
.expect("Failed to generate access token for test");
|
.unwrap();
|
||||||
|
|
||||||
// Test valid Bearer token
|
// Test valid Bearer token
|
||||||
let mut request = Request::builder()
|
let mut request = Request::builder()
|
||||||
.header(AUTHORIZATION, format!("Bearer {}", token))
|
.header(AUTHORIZATION, format!("Bearer {}", token))
|
||||||
.body(Body::empty())
|
.body(Body::empty())
|
||||||
.expect("Failed to build request for test");
|
.unwrap();
|
||||||
|
|
||||||
// Verify token parsing would work
|
// Verify token parsing would work
|
||||||
let auth_header = request
|
let auth_header = request
|
||||||
|
|||||||
@ -188,8 +188,7 @@ impl RefreshTokenClaims {
|
|||||||
/// Generate RSA key pair for JWT signing (RS256)
|
/// Generate RSA key pair for JWT signing (RS256)
|
||||||
pub fn generate_rsa_key_pair() -> Result<RsaKeys> {
|
pub fn generate_rsa_key_pair() -> Result<RsaKeys> {
|
||||||
use rsa::rand_core::OsRng;
|
use rsa::rand_core::OsRng;
|
||||||
use rsa::{RsaPrivateKey, RsaPublicKey};
|
use rsa::{pkcs1::EncodeRsaPrivateKey, pkcs1::EncodeRsaPublicKey, RsaPrivateKey, RsaPublicKey};
|
||||||
use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding};
|
|
||||||
|
|
||||||
// Generate 2048-bit RSA key pair with OS randomness for cryptographic security
|
// Generate 2048-bit RSA key pair with OS randomness for cryptographic security
|
||||||
let private_key =
|
let private_key =
|
||||||
@ -197,14 +196,14 @@ pub fn generate_rsa_key_pair() -> Result<RsaKeys> {
|
|||||||
|
|
||||||
let public_key = RsaPublicKey::from(&private_key);
|
let public_key = RsaPublicKey::from(&private_key);
|
||||||
|
|
||||||
// Convert to PKCS#8 PEM format (required by jsonwebtoken and signature crates)
|
// Convert to PEM format
|
||||||
let private_key_pem = private_key
|
let private_key_pem = private_key
|
||||||
.to_pkcs8_pem(LineEnding::LF)
|
.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)
|
||||||
.context("Failed to encode private key as PKCS#8 PEM")?
|
.context("Failed to encode private key as PEM")?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let public_key_pem = public_key
|
let public_key_pem = public_key
|
||||||
.to_public_key_pem(LineEnding::LF)
|
.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)
|
||||||
.context("Failed to encode public key as PEM")?;
|
.context("Failed to encode public key as PEM")?;
|
||||||
|
|
||||||
Ok(RsaKeys {
|
Ok(RsaKeys {
|
||||||
@ -238,23 +237,10 @@ pub fn load_rsa_keys_from_files(private_key_path: &str, public_key_path: &str) -
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn create_test_jwt_config() -> JwtConfig {
|
|
||||||
let keys = generate_rsa_key_pair().expect("Failed to generate test keys");
|
|
||||||
JwtConfig {
|
|
||||||
issuer: "test-issuer".to_string(),
|
|
||||||
audience: "test-audience".to_string(),
|
|
||||||
access_token_expiration_hours: 1,
|
|
||||||
refresh_token_expiration_hours: 24,
|
|
||||||
private_key_pem: keys.private_key_pem,
|
|
||||||
public_key_pem: keys.public_key_pem,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_jwt_token_generation_and_verification() {
|
async fn test_jwt_token_generation_and_verification() {
|
||||||
let config = create_test_jwt_config();
|
let config = JwtConfig::default();
|
||||||
let jwt_service = JwtService::new(config)
|
let jwt_service = JwtService::new(config).unwrap();
|
||||||
.expect("Failed to create JWT service for test");
|
|
||||||
|
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
let session_id = Uuid::new_v4();
|
let session_id = Uuid::new_v4();
|
||||||
@ -263,12 +249,12 @@ mod tests {
|
|||||||
// Generate tokens
|
// Generate tokens
|
||||||
let token_response = jwt_service
|
let token_response = jwt_service
|
||||||
.generate_token_pair(user_id, session_id, roles.clone())
|
.generate_token_pair(user_id, session_id, roles.clone())
|
||||||
.expect("Failed to generate token pair for test");
|
.unwrap();
|
||||||
|
|
||||||
// Verify access token
|
// Verify access token
|
||||||
let access_claims = jwt_service
|
let access_claims = jwt_service
|
||||||
.verify_access_token(&token_response.access_token)
|
.verify_access_token(&token_response.access_token)
|
||||||
.expect("Failed to verify access token for test");
|
.unwrap();
|
||||||
assert_eq!(access_claims.claims.sub, user_id.to_string());
|
assert_eq!(access_claims.claims.sub, user_id.to_string());
|
||||||
assert_eq!(access_claims.claims.session_id, session_id.to_string());
|
assert_eq!(access_claims.claims.session_id, session_id.to_string());
|
||||||
assert_eq!(access_claims.claims.roles, roles);
|
assert_eq!(access_claims.claims.roles, roles);
|
||||||
|
|||||||
@ -19,15 +19,50 @@ use control_center::auth::{
|
|||||||
user::{User, UserRole, UserService},
|
user::{User, UserRole, UserService},
|
||||||
AuthService,
|
AuthService,
|
||||||
};
|
};
|
||||||
use control_center::services::jwt::generate_rsa_key_pair;
|
|
||||||
|
|
||||||
/// Generate RSA key pair for testing
|
/// Generate RSA key pair for testing (pre-generated to avoid rand_core
|
||||||
|
/// conflict)
|
||||||
fn generate_test_keys() -> (Vec<u8>, Vec<u8>) {
|
fn generate_test_keys() -> (Vec<u8>, Vec<u8>) {
|
||||||
let keys = generate_rsa_key_pair().expect("Failed to generate test RSA keys");
|
let private_pem = b"-----BEGIN PRIVATE KEY-----
|
||||||
(
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7F43HxrVfJJ+k
|
||||||
keys.private_key_pem.into_bytes(),
|
DQMEjENGqJLnBn6MvJnCu93A4ZNKEEpPGX1Y6V+qiqLH5B7wNMIJ2QVnLjYjKZu5
|
||||||
keys.public_key_pem.into_bytes(),
|
K5LkVCH8N7N1s9QXiLI7TzQVEH2TQx3MzQFRmVzGlyVFxF0qMHf/mNlZvE0Hb9oI
|
||||||
)
|
YdYnBW5k4TRmzrN4THEbWqLcGVVfMVFQqG1j/V3G5dTpV1sN8a9xqJG0dKNmVP2M
|
||||||
|
H5JG3kQ8CxFqp7pRsGvqH9IVjB6KXj8L3VPQYJxIBKHGZMlsP9SVqH3vwqLLKcBj
|
||||||
|
G2UvhkVYJ6ZLvYIj7LvZKrCBrP3h5Q7hYpXZqDGYd5dDwWFJ3xkVMkJMGR3D3VrK
|
||||||
|
R1jvJTEhAgMBAAECggEAXV2TZjfRbhR0IaqU3bpSZvb+6uL+5OgWRO5VbG0XGFmv
|
||||||
|
1v+2F5hQEZYKKxZvTlF4IxN1J0YQRKt4BG6PZqN9+Uj8z7DVEDqiWLIZSEhUPzVu
|
||||||
|
E3IKZaEqvGKvEwBglmfqnE6cEZ2Kb9aOx1a5N8UH5q7xQ2d4YxKfYwRBkDYEZmZp
|
||||||
|
R4tEb0qfKFgIGmCfK5fNvJqXvH/f1xq9QW1qrGYqGAqh7bkxvXGfkRBXh7Q8sAXW
|
||||||
|
B2XQ8L9CZV5lsP0F7n8nKvDVKfqZfGXlZJZQvJPF5L8p6g7V7uE+6bZlF4YZLRvW
|
||||||
|
2aq2xZR6bGCWxBpLb2lG1QU0aJLqVmVjAQYDM9PB0QKBgQDfhKn8F5aITVFJhVVu
|
||||||
|
BN8F2hLRtFDfSPTN0JM1WZq/EqYdLjnlAY8L7kBBzNaKgOGBRqfO1lVv7DG4vPtX
|
||||||
|
nrLaM3SU3F0lC8tVwqJTUvZS5/Pl3wqAZRlWbWm4Yp6gWXlGPTaWRpGRBOqB+rWP
|
||||||
|
b8rX2SnZjvFlQZlZPfvHGWpkFQKBgQDYtvXAGNdj8hMZ6mMUKYv3YbF7F2xUlf1x
|
||||||
|
H0f8I5QGPiZoqzNGJXCEGvNVNEUgR7LY6AeqWtLqRKJsQ5NYb2XzLhA2BVOV1BV2
|
||||||
|
Z1Y3s0V5XjFE4q7gWmEcLlWyEiKiRCRQC4T7qB+gUQ8lFBrGqQ4vZqE+7pQyYfCt
|
||||||
|
1yFQnvqrQQKBgQCcvCvE4wMhF2lYFxM4v8qCDXJ9nLMKjYPFn3C0YrP6Zn2Pm7r3
|
||||||
|
fJGDVBmN5wVLDXqZnSrmh0iBs7PdBqeHALF7jTj9X8tNe+hpXwQ/GWSF8b3q1b6k
|
||||||
|
wZLhKpJQGpYH0M3l7B6rrM3Vr1E/DQjXvXdqXrL3vKfHQrEk3c9zVCkfTQKBgGVl
|
||||||
|
qdvBYvdgWHvPzL3+vqvLPVKDnLjN6hXA2cJGaBNpOlm+bI5cJLLnWhJrjCkK+POo
|
||||||
|
0oPKvnO5qKQjRDWLVCJH3z8MxPjqV6Yk1hAg/4vqV3oQsJDfU3qiDPAHrBbKBlRm
|
||||||
|
4KYmEqIhqNYJqXN7nnQFOLYFxOaXKqe6HBh1g4QBAoGBAIRXPwKJhwIUfLJ8DQAO
|
||||||
|
9z9YH3eIiGkLxF8f7WZvVnNwMEpXl4IqN8N3qGXKJfXJI1YfBKXN7LOqCOVFGkVq
|
||||||
|
JkVN8qPTwAKU8E2U3dGzQVPP2JmGPPNyZJxQJ3sYtKnLkz2YdEbKDhkG5wPDHKKu
|
||||||
|
LfEK1YMqL/VvAJdIXxqZqDXf
|
||||||
|
-----END PRIVATE KEY-----";
|
||||||
|
|
||||||
|
let public_pem = b"-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuxeNx8a1XySfpA0DBIxD
|
||||||
|
RqiS5wZ+jLyZwrvdwOGTShBKTxl9WOlfqoqix+Qe8DTCCdkFZy42IymbhSuS5FQh
|
||||||
|
/DezdLPUF4iyO080FRB9k0MdzM0BUZlcxpclRcRdKjB3/5jZWbxNB2/aCGHWJwVu
|
||||||
|
ZOE0Zs6zeExxG1qi3BlVXzFRUKhtY/1dxuXU6Vdbl/GvcaiRtHSjZlT9jB+SRt5E
|
||||||
|
PAsTaqe6UbBr6h/SFYweil4/C91T0GCcSAShxmTJbD/Ulah978KizynAYxtlL4ZF
|
||||||
|
WCemS72CI+y72SqwgaZ94eUO4WKV2agxmHeXQ8FhSd8ZFTJCTBKDW91Kykdo7yUx
|
||||||
|
IQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----";
|
||||||
|
|
||||||
|
(private_pem.to_vec(), public_pem.to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create JWT service for testing
|
/// Create JWT service for testing
|
||||||
@ -39,7 +74,7 @@ fn create_jwt_service() -> JwtService {
|
|||||||
"test-control-center",
|
"test-control-center",
|
||||||
vec!["orchestrator".to_string(), "cli".to_string()],
|
vec!["orchestrator".to_string(), "cli".to_string()],
|
||||||
)
|
)
|
||||||
.expect("Failed to create JWT service for tests")
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -362,7 +397,7 @@ async fn test_full_auth_flow() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_signature_detection() {
|
fn test_invalid_signature_detection() {
|
||||||
let (private_key1, public_key1) = generate_test_keys();
|
let (private_key1, public_key1) = generate_test_keys();
|
||||||
let (_private_key2, public_key2) = generate_test_keys(); // Different key pair
|
let (private_key2, _) = generate_test_keys(); // Different key pair
|
||||||
|
|
||||||
// Service 1 generates token
|
// Service 1 generates token
|
||||||
let jwt_service1 = JwtService::new(
|
let jwt_service1 = JwtService::new(
|
||||||
@ -371,23 +406,22 @@ fn test_invalid_signature_detection() {
|
|||||||
"test-issuer",
|
"test-issuer",
|
||||||
vec!["test-audience".to_string()],
|
vec!["test-audience".to_string()],
|
||||||
)
|
)
|
||||||
.expect("Failed to create jwt_service1");
|
.unwrap();
|
||||||
|
|
||||||
let token_pair = jwt_service1
|
let token_pair = jwt_service1
|
||||||
.generate_token_pair("user123", "workspace1", "perm_hash", None)
|
.generate_token_pair("user123", "workspace1", "perm_hash", None)
|
||||||
.expect("Failed to generate token pair");
|
.unwrap();
|
||||||
|
|
||||||
// Service 2 with different public key tries to validate
|
// Service 2 with different keys tries to validate
|
||||||
// This should fail because the token was signed with key1 but we're validating with key2
|
|
||||||
let jwt_service2 = JwtService::new(
|
let jwt_service2 = JwtService::new(
|
||||||
&private_key1,
|
&private_key2,
|
||||||
&public_key2, // Different public key!
|
&public_key1,
|
||||||
"test-issuer",
|
"test-issuer",
|
||||||
vec!["test-audience".to_string()],
|
vec!["test-audience".to_string()],
|
||||||
)
|
)
|
||||||
.expect("Failed to create jwt_service2");
|
.unwrap();
|
||||||
|
|
||||||
// Should fail signature verification because public keys don't match
|
// Should fail signature verification
|
||||||
let validation_result = jwt_service2.validate_token(&token_pair.access_token);
|
let validation_result = jwt_service2.validate_token(&token_pair.access_token);
|
||||||
assert!(validation_result.is_err());
|
assert!(validation_result.is_err());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,35 +1,20 @@
|
|||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::http::{Request, StatusCode};
|
use axum::http::{Request, StatusCode};
|
||||||
use extension_registry::{build_routes, AppState, Config};
|
use extension_registry::{build_routes, AppState, Config};
|
||||||
use extension_registry::config::OciConfig;
|
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
/// Create a minimal test config with a mock OCI backend
|
#[tokio::test]
|
||||||
fn create_test_config() -> Config {
|
async fn test_health_check() {
|
||||||
Config {
|
let config = Config {
|
||||||
server: extension_registry::config::ServerConfig::default(),
|
server: extension_registry::config::ServerConfig::default(),
|
||||||
gitea: None,
|
gitea: None,
|
||||||
// Use OCI as test backend (doesn't require file validation for auth_token_path)
|
oci: None,
|
||||||
oci: Some(OciConfig {
|
|
||||||
id: Some("test-oci".to_string()),
|
|
||||||
registry: "localhost:5000".to_string(),
|
|
||||||
namespace: "test".to_string(),
|
|
||||||
auth_token_path: None,
|
|
||||||
timeout_seconds: 30,
|
|
||||||
verify_ssl: false,
|
|
||||||
}),
|
|
||||||
sources: extension_registry::config::SourcesConfig::default(),
|
sources: extension_registry::config::SourcesConfig::default(),
|
||||||
distributions: extension_registry::config::DistributionsConfig::default(),
|
distributions: extension_registry::config::DistributionsConfig::default(),
|
||||||
cache: extension_registry::config::CacheConfig::default(),
|
cache: extension_registry::config::CacheConfig::default(),
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[ignore] // Requires OCI registry or Gitea service to be running
|
|
||||||
#[ignore] // Requires OCI registry service to be running
|
|
||||||
async fn test_health_check() {
|
|
||||||
let config = create_test_config();
|
|
||||||
let state = AppState::new(config).expect("Failed to create app state");
|
let state = AppState::new(config).expect("Failed to create app state");
|
||||||
let app = build_routes(state);
|
let app = build_routes(state);
|
||||||
|
|
||||||
@ -54,9 +39,16 @@ async fn test_health_check() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore] // Requires OCI registry or Gitea service to be running
|
|
||||||
async fn test_list_extensions_empty() {
|
async fn test_list_extensions_empty() {
|
||||||
let config = create_test_config();
|
let config = Config {
|
||||||
|
server: extension_registry::config::ServerConfig::default(),
|
||||||
|
gitea: None,
|
||||||
|
oci: None,
|
||||||
|
sources: extension_registry::config::SourcesConfig::default(),
|
||||||
|
distributions: extension_registry::config::DistributionsConfig::default(),
|
||||||
|
cache: extension_registry::config::CacheConfig::default(),
|
||||||
|
};
|
||||||
|
|
||||||
let state = AppState::new(config).expect("Failed to create app state");
|
let state = AppState::new(config).expect("Failed to create app state");
|
||||||
let app = build_routes(state);
|
let app = build_routes(state);
|
||||||
|
|
||||||
@ -80,9 +72,15 @@ async fn test_list_extensions_empty() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore] // Requires OCI registry or Gitea service to be running
|
|
||||||
async fn test_get_nonexistent_extension() {
|
async fn test_get_nonexistent_extension() {
|
||||||
let config = create_test_config();
|
let config = Config {
|
||||||
|
server: extension_registry::config::ServerConfig::default(),
|
||||||
|
gitea: None,
|
||||||
|
oci: None,
|
||||||
|
sources: extension_registry::config::SourcesConfig::default(),
|
||||||
|
distributions: extension_registry::config::DistributionsConfig::default(),
|
||||||
|
cache: extension_registry::config::CacheConfig::default(),
|
||||||
|
};
|
||||||
|
|
||||||
let state = AppState::new(config).expect("Failed to create app state");
|
let state = AppState::new(config).expect("Failed to create app state");
|
||||||
let app = build_routes(state);
|
let app = build_routes(state);
|
||||||
@ -101,9 +99,15 @@ async fn test_get_nonexistent_extension() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore] // Requires OCI registry or Gitea service to be running
|
|
||||||
async fn test_metrics_endpoint() {
|
async fn test_metrics_endpoint() {
|
||||||
let config = create_test_config();
|
let config = Config {
|
||||||
|
server: extension_registry::config::ServerConfig::default(),
|
||||||
|
gitea: None,
|
||||||
|
oci: None,
|
||||||
|
sources: extension_registry::config::SourcesConfig::default(),
|
||||||
|
distributions: extension_registry::config::DistributionsConfig::default(),
|
||||||
|
cache: extension_registry::config::CacheConfig::default(),
|
||||||
|
};
|
||||||
|
|
||||||
let state = AppState::new(config).expect("Failed to create app state");
|
let state = AppState::new(config).expect("Failed to create app state");
|
||||||
let app = build_routes(state);
|
let app = build_routes(state);
|
||||||
@ -127,9 +131,15 @@ async fn test_metrics_endpoint() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore] // Requires OCI registry or Gitea service to be running
|
|
||||||
async fn test_cache_stats_endpoint() {
|
async fn test_cache_stats_endpoint() {
|
||||||
let config = create_test_config();
|
let config = Config {
|
||||||
|
server: extension_registry::config::ServerConfig::default(),
|
||||||
|
gitea: None,
|
||||||
|
oci: None,
|
||||||
|
sources: extension_registry::config::SourcesConfig::default(),
|
||||||
|
distributions: extension_registry::config::DistributionsConfig::default(),
|
||||||
|
cache: extension_registry::config::CacheConfig::default(),
|
||||||
|
};
|
||||||
|
|
||||||
let state = AppState::new(config).expect("Failed to create app state");
|
let state = AppState::new(config).expect("Failed to create app state");
|
||||||
let app = build_routes(state);
|
let app = build_routes(state);
|
||||||
@ -155,9 +165,15 @@ async fn test_cache_stats_endpoint() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore] // Requires OCI registry or Gitea service to be running
|
|
||||||
async fn test_invalid_extension_type() {
|
async fn test_invalid_extension_type() {
|
||||||
let config = create_test_config();
|
let config = Config {
|
||||||
|
server: extension_registry::config::ServerConfig::default(),
|
||||||
|
gitea: None,
|
||||||
|
oci: None,
|
||||||
|
sources: extension_registry::config::SourcesConfig::default(),
|
||||||
|
distributions: extension_registry::config::DistributionsConfig::default(),
|
||||||
|
cache: extension_registry::config::CacheConfig::default(),
|
||||||
|
};
|
||||||
|
|
||||||
let state = AppState::new(config).expect("Failed to create app state");
|
let state = AppState::new(config).expect("Failed to create app state");
|
||||||
let app = build_routes(state);
|
let app = build_routes(state);
|
||||||
|
|||||||
@ -138,64 +138,31 @@ async fn test_components(engine: &ProvisioningEngine, tools: &ProvisioningTools)
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::fs;
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
/// Create a test config with a temporary provisioning path
|
|
||||||
fn create_test_config() -> (Config, TempDir) {
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
|
||||||
let temp_path = temp_dir.path();
|
|
||||||
|
|
||||||
// Create minimal provisioning directory structure for tests
|
|
||||||
let core_path = temp_path.join("core/nulib");
|
|
||||||
fs::create_dir_all(&core_path).expect("Failed to create test provisioning dir");
|
|
||||||
|
|
||||||
// Create the provisioning script file
|
|
||||||
let script_path = core_path.join("provisioning");
|
|
||||||
fs::write(&script_path, "#!/bin/bash\necho 'test provisioning script'")
|
|
||||||
.expect("Failed to create test provisioning script");
|
|
||||||
|
|
||||||
let config = Config {
|
|
||||||
provisioning_path: temp_path.to_path_buf(),
|
|
||||||
ai: config::AIConfig::default(),
|
|
||||||
server: config::ServerConfig::default(),
|
|
||||||
debug: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
(config, temp_dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_server_creation() {
|
async fn test_server_creation() {
|
||||||
let (config, _temp) = create_test_config();
|
let config = Config::default();
|
||||||
|
let engine = ProvisioningEngine::new(&config).expect("Failed to create engine");
|
||||||
let tools = ProvisioningTools::new(&config);
|
let tools = ProvisioningTools::new(&config);
|
||||||
|
|
||||||
// Test parsing - the legacy implementation returns the description as-is
|
// Test parsing
|
||||||
let result = tools.parse_server_description("Create 1 server for web hosting");
|
let result = tools.parse_server_description("Create 1 server for web hosting");
|
||||||
assert!(result.is_ok(), "Failed to parse server description");
|
assert!(result.is_ok());
|
||||||
|
|
||||||
let server_config = result.unwrap();
|
let server_config = result.unwrap();
|
||||||
// The legacy parse_server_description returns {"description": "..."}
|
assert_eq!(server_config["count"], 1);
|
||||||
assert!(server_config.get("description").is_some(), "Missing description field");
|
|
||||||
assert_eq!(
|
|
||||||
server_config["description"].as_str(),
|
|
||||||
Some("Create 1 server for web hosting"),
|
|
||||||
"Description mismatch"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_loading() {
|
fn test_config_loading() {
|
||||||
let (config, _temp) = create_test_config();
|
let config = Config::default();
|
||||||
assert!(!config.debug, "Debug should be false by default");
|
assert!(!config.debug);
|
||||||
assert!(
|
assert!(
|
||||||
config.provisioning_path.exists(),
|
config.provisioning_path.exists()
|
||||||
"Provisioning path should exist (it's a temp dir)"
|
|| !config
|
||||||
);
|
.provisioning_path
|
||||||
assert_eq!(
|
.to_string_lossy()
|
||||||
config.provisioning_path.to_string_lossy().ends_with("provisioning"),
|
.contains("/usr/local/provisioning")
|
||||||
false,
|
|
||||||
"Test path should not end with /provisioning"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,26 +13,17 @@
|
|||||||
//! - Export to common SIEM formats (Splunk, ELK)
|
//! - Export to common SIEM formats (Splunk, ELK)
|
||||||
//!
|
//!
|
||||||
//! # Example
|
//! # Example
|
||||||
//! ```ignore
|
//! ```no_run
|
||||||
//! use provisioning_orchestrator::audit::{AuditLogger, ActionType, AuditLoggerConfig};
|
//! use audit::{AuditLogger, AuditEvent, ActionType};
|
||||||
//! use std::path::PathBuf;
|
|
||||||
//!
|
//!
|
||||||
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
//! let config = AuditLoggerConfig {
|
|
||||||
//! log_dir: PathBuf::from("/var/log/audit"),
|
|
||||||
//! enable_syslog: false,
|
|
||||||
//! ..Default::default()
|
|
||||||
//! };
|
|
||||||
//! let logger = AuditLogger::new(config).await?;
|
//! let logger = AuditLogger::new(config).await?;
|
||||||
//!
|
//!
|
||||||
//! logger.log_action(
|
//! logger.log_action(
|
||||||
//! "user123",
|
//! user_id,
|
||||||
//! ActionType::ServerCreate,
|
//! ActionType::ServerCreate,
|
||||||
//! "my-infrastructure",
|
//! "my-infrastructure",
|
||||||
//! serde_json::json!({"count": 3}),
|
//! serde_json::json!({"count": 3}),
|
||||||
//! ).await?;
|
//! ).await?;
|
||||||
//! # Ok(())
|
|
||||||
//! # }
|
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
|
|||||||
@ -12,11 +12,9 @@
|
|||||||
//! - Automated compliance reporting
|
//! - Automated compliance reporting
|
||||||
//!
|
//!
|
||||||
//! # Example
|
//! # Example
|
||||||
//! ```ignore
|
//! ```no_run
|
||||||
//! use provisioning_orchestrator::compliance::{ComplianceService, ComplianceConfig};
|
//! use compliance::{ComplianceService, GdprService};
|
||||||
//!
|
//!
|
||||||
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
//! let config = ComplianceConfig::default();
|
|
||||||
//! let compliance = ComplianceService::new(config).await?;
|
//! let compliance = ComplianceService::new(config).await?;
|
||||||
//!
|
//!
|
||||||
//! // Export user data for GDPR compliance
|
//! // Export user data for GDPR compliance
|
||||||
@ -24,8 +22,6 @@
|
|||||||
//!
|
//!
|
||||||
//! // Generate SOC2 report
|
//! // Generate SOC2 report
|
||||||
//! let report = compliance.soc2.generate_report().await?;
|
//! let report = compliance.soc2.generate_report().await?;
|
||||||
//! # Ok(())
|
|
||||||
//! # }
|
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
pub mod access_control;
|
pub mod access_control;
|
||||||
|
|||||||
@ -20,7 +20,6 @@ fn create_test_user(id: &str, team: &str, role: Role) -> User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore] // Hangs: service.start() spawns background tasks that don't shut down cleanly
|
|
||||||
async fn test_complete_break_glass_workflow() {
|
async fn test_complete_break_glass_workflow() {
|
||||||
// Setup
|
// Setup
|
||||||
let config = BreakGlassConfig::default();
|
let config = BreakGlassConfig::default();
|
||||||
@ -195,7 +194,6 @@ async fn test_approval_validation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore] // Hangs due to background task spawned by service.start() not completing
|
|
||||||
async fn test_auto_revocation_expired() {
|
async fn test_auto_revocation_expired() {
|
||||||
let mut config = BreakGlassConfig::default();
|
let mut config = BreakGlassConfig::default();
|
||||||
config.max_session_duration_hours = 0; // Immediate expiration
|
config.max_session_duration_hours = 0; // Immediate expiration
|
||||||
|
|||||||
@ -39,7 +39,6 @@ async fn test_service_registration() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore] // Hangs: resolve_startup_order has an infinite loop or deadlock
|
|
||||||
async fn test_dependency_resolution_simple() {
|
async fn test_dependency_resolution_simple() {
|
||||||
let orchestrator = ServiceOrchestrator::new(
|
let orchestrator = ServiceOrchestrator::new(
|
||||||
"/usr/local/bin/nu".to_string(),
|
"/usr/local/bin/nu".to_string(),
|
||||||
@ -84,7 +83,6 @@ async fn test_dependency_resolution_simple() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore] // Hangs: resolve_startup_order has an infinite loop or deadlock
|
|
||||||
async fn test_dependency_resolution_complex() {
|
async fn test_dependency_resolution_complex() {
|
||||||
let orchestrator = ServiceOrchestrator::new(
|
let orchestrator = ServiceOrchestrator::new(
|
||||||
"/usr/local/bin/nu".to_string(),
|
"/usr/local/bin/nu".to_string(),
|
||||||
|
|||||||
@ -363,11 +363,6 @@ fn test_empty_config_file_toml() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_environment_partial_override() {
|
fn test_environment_partial_override() {
|
||||||
// Clean up all relevant environment variables to ensure test isolation
|
|
||||||
env::remove_var("TEST_SERVICE_NAME");
|
|
||||||
env::remove_var("TEST_SERVICE_PORT");
|
|
||||||
env::remove_var("TEST_SERVICE_ENABLED");
|
|
||||||
|
|
||||||
// Only override one field
|
// Only override one field
|
||||||
env::set_var("TEST_SERVICE_PORT", "5555");
|
env::set_var("TEST_SERVICE_PORT", "5555");
|
||||||
|
|
||||||
@ -385,7 +380,6 @@ fn test_environment_partial_override() {
|
|||||||
assert_eq!(config.port, 5555); // overridden
|
assert_eq!(config.port, 5555); // overridden
|
||||||
assert!(config.enabled); // unchanged
|
assert!(config.enabled); // unchanged
|
||||||
|
|
||||||
// Clean up after test
|
|
||||||
env::remove_var("TEST_SERVICE_PORT");
|
env::remove_var("TEST_SERVICE_PORT");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -80,7 +80,6 @@ impl ConfigLoader for McpServerConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore] // Requires Nickel files to exist in /tmp/
|
|
||||||
fn test_load_mcp_server_from_nickel() {
|
fn test_load_mcp_server_from_nickel() {
|
||||||
let ncl_file = "/tmp/mcp-server.solo.ncl";
|
let ncl_file = "/tmp/mcp-server.solo.ncl";
|
||||||
|
|
||||||
@ -211,7 +210,6 @@ impl ConfigLoader for RagConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore] // Requires Nickel files to exist in /tmp/
|
|
||||||
fn test_load_rag_from_nickel() {
|
fn test_load_rag_from_nickel() {
|
||||||
let ncl_file = "/tmp/rag.solo.ncl";
|
let ncl_file = "/tmp/rag.solo.ncl";
|
||||||
|
|
||||||
@ -317,7 +315,6 @@ impl ConfigLoader for ExtensionRegistryConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore] // Requires Nickel files to exist in /tmp/
|
|
||||||
fn test_load_extension_registry_from_nickel() {
|
fn test_load_extension_registry_from_nickel() {
|
||||||
let ncl_file = "/tmp/extension-registry.solo.ncl";
|
let ncl_file = "/tmp/extension-registry.solo.ncl";
|
||||||
|
|
||||||
@ -341,7 +338,6 @@ fn test_load_extension_registry_from_nickel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore] // Requires Nickel files to exist in /tmp/
|
|
||||||
fn test_load_control_center_from_nickel() {
|
fn test_load_control_center_from_nickel() {
|
||||||
let ncl_file = "/tmp/control-center.solo.ncl";
|
let ncl_file = "/tmp/control-center.solo.ncl";
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
//!
|
//!
|
||||||
//! # Example
|
//! # Example
|
||||||
//!
|
//!
|
||||||
//! ```ignore
|
//! ```no_run
|
||||||
//! use service_clients::MachinesClient;
|
//! use service_clients::MachinesClient;
|
||||||
//!
|
//!
|
||||||
//! #[tokio::main]
|
//! #[tokio::main]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user