#![cfg(feature = "git")]
use bashkit::{Bash, GitConfig};
fn create_git_bash() -> Bash {
Bash::builder()
.git(GitConfig::new().author("Sandbox User", "sandbox@test.local"))
.build()
}
mod identity_security {
use super::*;
#[tokio::test]
async fn test_commit_uses_sandbox_identity() {
let mut bash = create_git_bash();
bash.exec("git init /repo && cd /repo && echo 'x' > x.txt && git add x.txt && git commit -m 'Test'").await.unwrap();
let log = bash.exec("cd /repo && git log").await.unwrap();
assert!(log.stdout.contains("Sandbox User"));
assert!(log.stdout.contains("sandbox@test.local"));
assert!(!log.stdout.contains("root@"));
}
#[tokio::test]
async fn test_custom_author_in_commits() {
let mut bash = Bash::builder()
.git(GitConfig::new().author("CI Bot", "ci@company.com"))
.build();
bash.exec("git init /repo && cd /repo && echo 'x' > x.txt && git add . && git commit -m 'CI commit'").await.unwrap();
let log = bash.exec("cd /repo && git log").await.unwrap();
assert!(log.stdout.contains("CI Bot"));
assert!(log.stdout.contains("ci@company.com"));
}
#[tokio::test]
async fn test_config_only_reads_repo_config() {
let mut bash = create_git_bash();
bash.exec("git init /repo").await.unwrap();
let result = bash.exec("cd /repo && git config user.name").await.unwrap();
assert_eq!(result.stdout.trim(), "Sandbox User");
bash.exec("cd /repo && git config user.name 'Repo User'")
.await
.unwrap();
let result = bash.exec("cd /repo && git config user.name").await.unwrap();
assert_eq!(result.stdout.trim(), "Repo User");
}
}
mod vfs_isolation {
use super::*;
#[tokio::test]
async fn test_git_operations_confined_to_vfs() {
let mut bash = create_git_bash();
let result = bash.exec("git init /repo").await.unwrap();
assert_eq!(result.exit_code, 0);
let result = bash.exec("ls -la /repo/.git").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("HEAD"));
assert!(result.stdout.contains("config"));
}
#[tokio::test]
async fn test_git_init_path_traversal_blocked() {
let mut bash = create_git_bash();
let result = bash.exec("git init /repo/../../../etc").await.unwrap();
assert_eq!(result.exit_code, 0);
let result = bash.exec("ls /etc/.git").await.unwrap();
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn test_git_add_vfs_only() {
let mut bash = create_git_bash();
bash.exec("git init /repo && cd /repo && echo 'content' > file.txt")
.await
.unwrap();
let result = bash.exec("cd /repo && git add file.txt").await.unwrap();
assert_eq!(result.exit_code, 0);
let status = bash.exec("cd /repo && git status").await.unwrap();
assert!(status.stdout.contains("file.txt"));
}
}
mod resource_limits {
use super::*;
use bashkit::ExecutionLimits;
#[tokio::test]
async fn test_fs_limits_apply_to_git() {
let mut bash = Bash::builder()
.git(GitConfig::new())
.limits(ExecutionLimits::new().max_commands(100))
.build();
let result = bash.exec("git init /repo").await.unwrap();
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn test_git_log_limit() {
let mut bash = create_git_bash();
bash.exec("git init /repo && cd /repo").await.unwrap();
for i in 1..=5 {
bash.exec(&format!(
"cd /repo && echo '{}' > file{}.txt && git add . && git commit -m 'Commit {}'",
i, i, i
))
.await
.unwrap();
}
let result = bash.exec("cd /repo && git log -n 2").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("Commit 5"));
assert!(result.stdout.contains("Commit 4"));
assert!(!result.stdout.contains("Commit 1"));
}
}
mod error_message_safety {
use super::*;
#[tokio::test]
async fn test_error_messages_no_real_paths() {
let mut bash = create_git_bash();
let result = bash.exec("cd /nonexistent && git status").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(!result.stderr.contains("/home/"));
assert!(!result.stderr.contains("/Users/"));
assert!(!result.stderr.contains("C:\\"));
}
#[tokio::test]
async fn test_error_messages_user_friendly() {
let mut bash = create_git_bash();
let result = bash.exec("cd /tmp && git status").await.unwrap();
assert!(result.stderr.contains("not a git repository"));
bash.exec("git init /repo").await.unwrap();
let result = bash
.exec("cd /repo && git commit -m 'Empty'")
.await
.unwrap();
assert!(result.stderr.contains("nothing to commit"));
}
}
mod git_not_configured {
use bashkit::Bash;
#[tokio::test]
async fn test_git_disabled_by_default() {
let mut bash = Bash::new();
let result = bash.exec("git init /repo").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("not configured"));
}
#[tokio::test]
async fn test_git_not_configured_helpful_message() {
let mut bash = Bash::new();
let result = bash.exec("git status").await.unwrap();
assert!(result.stderr.contains("Bash::builder().git()"));
}
}
mod concurrent_safety {
use super::*;
#[tokio::test]
async fn test_isolated_repositories() {
let mut bash1 = Bash::builder()
.git(GitConfig::new().author("User1", "user1@test.com"))
.build();
let mut bash2 = Bash::builder()
.git(GitConfig::new().author("User2", "user2@test.com"))
.build();
bash1.exec("git init /repo1 && cd /repo1 && echo 'a' > a.txt && git add . && git commit -m 'Repo1'").await.unwrap();
bash2.exec("git init /repo2 && cd /repo2 && echo 'b' > b.txt && git add . && git commit -m 'Repo2'").await.unwrap();
let log1 = bash1.exec("cd /repo1 && git log").await.unwrap();
assert!(log1.stdout.contains("User1"));
assert!(!log1.stdout.contains("User2"));
let log2 = bash2.exec("cd /repo2 && git log").await.unwrap();
assert!(log2.stdout.contains("User2"));
assert!(!log2.stdout.contains("User1"));
}
}