#![cfg(feature = "git")]
use bashkit::{Bash, GitConfig};
fn create_git_bash_with_allowlist(allowed: &[&str]) -> Bash {
let mut config = GitConfig::new().author("Test User", "test@example.com");
for pattern in allowed {
config = config.allow_remote(*pattern);
}
Bash::builder().git(config).build()
}
fn create_git_bash_allow_all() -> Bash {
Bash::builder()
.git(
GitConfig::new()
.author("Test User", "test@example.com")
.allow_all_remotes(),
)
.build()
}
fn create_git_bash_no_remote() -> Bash {
Bash::builder()
.git(GitConfig::new().author("Test User", "test@example.com"))
.build()
}
mod url_allowlist {
use super::*;
#[tokio::test]
async fn test_empty_allowlist_blocks_all() {
let mut bash = create_git_bash_no_remote();
bash.exec("git init /repo").await.unwrap();
let result = bash
.exec("cd /repo && git remote add origin https://github.com/org/repo.git")
.await
.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("no remote URLs are allowed"));
}
#[tokio::test]
async fn test_allowlist_pattern_matching() {
let mut bash = create_git_bash_with_allowlist(&["https://github.com/allowed-org/"]);
bash.exec("git init /repo").await.unwrap();
let result = bash
.exec("cd /repo && git remote add origin https://github.com/allowed-org/repo.git")
.await
.unwrap();
assert_eq!(result.exit_code, 0);
let result = bash
.exec("cd /repo && git remote add other https://github.com/other-org/repo.git")
.await
.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("not in allowlist"));
}
#[tokio::test]
async fn test_multiple_allowlist_patterns() {
let mut bash = create_git_bash_with_allowlist(&[
"https://github.com/org1/",
"https://github.com/org2/",
"https://gitlab.com/",
]);
bash.exec("git init /repo").await.unwrap();
let r1 = bash
.exec("cd /repo && git remote add gh1 https://github.com/org1/repo.git")
.await
.unwrap();
assert_eq!(r1.exit_code, 0);
let r2 = bash
.exec("cd /repo && git remote add gh2 https://github.com/org2/repo.git")
.await
.unwrap();
assert_eq!(r2.exit_code, 0);
let r3 = bash
.exec("cd /repo && git remote add gl https://gitlab.com/any/repo.git")
.await
.unwrap();
assert_eq!(r3.exit_code, 0);
let r4 = bash
.exec("cd /repo && git remote add bitbucket https://bitbucket.org/any/repo.git")
.await
.unwrap();
assert_ne!(r4.exit_code, 0);
}
}
mod protocol_enforcement {
use super::*;
#[tokio::test]
async fn test_ssh_urls_blocked() {
let mut bash = create_git_bash_allow_all();
bash.exec("git init /repo").await.unwrap();
let result = bash
.exec("cd /repo && git remote add origin git@github.com:org/repo.git")
.await
.unwrap();
assert_ne!(result.exit_code, 0);
assert!(
result.stderr.contains("only HTTPS URLs are allowed")
|| result.stderr.contains("invalid remote URL"),
"unexpected stderr: {}",
result.stderr
);
}
#[tokio::test]
async fn test_git_protocol_blocked() {
let mut bash = create_git_bash_allow_all();
bash.exec("git init /repo").await.unwrap();
let result = bash
.exec("cd /repo && git remote add origin git://github.com/org/repo.git")
.await
.unwrap();
assert_ne!(result.exit_code, 0);
assert!(
result.stderr.contains("only HTTPS URLs are allowed")
|| result.stderr.contains("invalid remote URL"),
"unexpected stderr: {}",
result.stderr
);
}
#[tokio::test]
async fn test_https_allowed() {
let mut bash = create_git_bash_allow_all();
bash.exec("git init /repo").await.unwrap();
let result = bash
.exec("cd /repo && git remote add origin https://github.com/org/repo.git")
.await
.unwrap();
assert_eq!(result.exit_code, 0);
}
}
mod remote_management {
use super::*;
#[tokio::test]
async fn test_remote_add_remove() {
let mut bash = create_git_bash_allow_all();
bash.exec("git init /repo").await.unwrap();
let result = bash
.exec("cd /repo && git remote add origin https://github.com/org/repo.git")
.await
.unwrap();
assert_eq!(result.exit_code, 0);
let result = bash.exec("cd /repo && git remote").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("origin"));
let result = bash.exec("cd /repo && git remote -v").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("https://github.com/org/repo.git"));
let result = bash
.exec("cd /repo && git remote remove origin")
.await
.unwrap();
assert_eq!(result.exit_code, 0);
let result = bash.exec("cd /repo && git remote").await.unwrap();
assert!(!result.stdout.contains("origin"));
}
#[tokio::test]
async fn test_remote_add_duplicate() {
let mut bash = create_git_bash_allow_all();
bash.exec("git init /repo").await.unwrap();
bash.exec("cd /repo && git remote add origin https://github.com/org/repo.git")
.await
.unwrap();
let result = bash
.exec("cd /repo && git remote add origin https://github.com/other/repo.git")
.await
.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("already exists"));
}
#[tokio::test]
async fn test_remote_remove_nonexistent() {
let mut bash = create_git_bash_allow_all();
bash.exec("git init /repo").await.unwrap();
let result = bash
.exec("cd /repo && git remote remove nonexistent")
.await
.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("no such remote"));
}
}
mod network_operations {
use super::*;
#[tokio::test]
async fn test_clone_sandbox_message() {
let mut bash = create_git_bash_allow_all();
let result = bash
.exec("git clone https://github.com/org/repo.git")
.await
.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("network operations not supported"));
assert!(result.stderr.contains("passed allowlist validation"));
}
#[tokio::test]
async fn test_clone_invalid_url() {
let mut bash = create_git_bash_with_allowlist(&["https://github.com/allowed/"]);
let result = bash
.exec("git clone https://github.com/blocked/repo.git")
.await
.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("not in allowlist"));
}
#[tokio::test]
async fn test_fetch_push_pull_validate_url() {
let mut bash = create_git_bash_allow_all();
bash.exec("git init /repo").await.unwrap();
bash.exec("cd /repo && git remote add origin https://github.com/org/repo.git")
.await
.unwrap();
let result = bash.exec("cd /repo && git fetch origin").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("network operations not supported"));
assert!(result.stderr.contains("passed allowlist validation"));
let result = bash.exec("cd /repo && git push origin").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("network operations not supported"));
let result = bash.exec("cd /repo && git pull origin").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("network operations not supported"));
}
#[tokio::test]
async fn test_network_ops_nonexistent_remote() {
let mut bash = create_git_bash_allow_all();
bash.exec("git init /repo").await.unwrap();
let result = bash
.exec("cd /repo && git fetch nonexistent")
.await
.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("not found"));
let result = bash.exec("cd /repo && git push nonexistent").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("not found"));
}
}